Spring Boot를 공부하면 거의 모든 Controller에서 아래와 같은 코드를 보게 된다.
@RestController
@RequiredArgsConstructor
public class ScheduleController {
private final ScheduleService scheduleService;
}
처음에는 @RequiredArgsConstructor가 단순히 “생성자를 만들어주는 어노테이션” 정도로만 이해했다.
하지만 공부를 하다 보니 이 어노테이션은 Spring의 생성자 주입(Constructor Injection)과 밀접한 관계가 있다는 것을 알게 되었다.
이번 글에서는 @RequiredArgsConstructor가 무엇인지, 왜 사용하는지, 그리고 Spring 내부에서는 어떤 방식으로 동작하는지 정리해보려고 한다.
@RequiredArgsConstructor란 ?
@RequiredArgsConstructor는 Lombok에서 제공하는 어노테이션이다.
이 어노테이션은 필수(required) 필드만을 매개변수로 받는 생성자를 자동으로 생성해준다.
⭐️여기서 “필수 필드”란 다음 두 가지를 의미한다.
- final 필드
- @NonNull이 붙은 필드
Spring에서는 대부분 final 필드를 사용하기 때문에 사실상 final 필드를 위한 생성자를 자동 생성한다고 이해해도 된다.
예를 들어 다음과 같은 코드가 있다고 가정해보자.
@RestController
@RequiredArgsConstructor
public class ScheduleController {
private final ScheduleService scheduleService;
}
겉으로 보기에는 생성자가 존재하지 않는다.
하지만 실제 컴파일이 되면 Lombok이 아래와 같은 생성자를 자동으로 만들어준다.
@RestController
public class ScheduleController {
private final ScheduleService scheduleService;
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
}
즉,
@RequiredArgsConstructor
한 줄이 위 생성자를 대신 작성해주는 것이다.
왜 이름이 RequiredArgsConstructor일까?
어노테이션 이름을 그대로 해석하면 의미를 이해하기 쉽다.
Required 필요한
Args Arguments(매개변수)
Constructor 생성자
즉,
필요한 값을 매개변수로 받는 생성자를 만들어준다.
라는 의미이다.
왜 final 필드는 생성자가 반드시 필요할까?
다음 코드를 보자.
public class ScheduleController {
private final ScheduleService scheduleService;
}
이 코드는 컴파일조차 되지 않는다.
그 이유는 final 필드는 반드시 초기화되어야 하기 때문이다.
final이 붙은 변수는 한 번만 값을 저장할 수 있다.
따라서 객체가 생성되는 순간 반드시 값을 넣어주어야 한다.
그래서 원래는 아래와 같은 생성자를 직접 작성해야 한다.
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
하지만 이런 생성자는 대부분 동일한 형태가 반복된다.
Lombok은 이런 반복 코드를 대신 작성해주는 것이다.
Spring에서는 왜 많이 사용할까?
Controller는 혼자서 일을 하지 않는다.
예를 들어 일정을 생성하는 기능이 있다면 Controller는 Service에게 일을 맡긴다.
@PostMapping
public ScheduleResponse createSchedule(@RequestBody ScheduleCreateRequest request) {
return scheduleService.createSchedule(request);
}
여기서 scheduleService가 존재하지 않으면 Controller는 아무 일도 할 수 없다.
즉, Controller에게 ScheduleService는 반드시 필요한 객체이다.
그래서 다음과 같이 선언한다.
private final ScheduleService scheduleService;
이 코드의 의미는 다음과 같다.
ScheduleController는 ScheduleService 없이는 정상적으로 동작할 수 없다.
그런데 new를 사용하지 않았다.
여기서 한 가지 의문이 생긴다.
우리는 어디에서도
new ScheduleController(...)
를 작성하지 않았다.
그런데 어떻게 ScheduleController 객체가 생성될까?
그 이유는 Spring이 대신 객체를 생성하기 때문이다.
@RestController가 붙은 클래스를 발견하면 Spring은 이 클래스를 Bean으로 등록한다.
그 다음 생성자를 확인한다.
public ScheduleController(ScheduleService scheduleService)
생성자를 확인한 Spring은 다음과 같이 판단한다.
ScheduleController를 만들려면
ScheduleService가 필요하구나.
그러면 Spring Container 안에서 ScheduleService Bean을 찾아 생성자에 넣어준다.
즉 내부적으로는 다음과 같은 과정이 일어난다.
1. Spring이 ScheduleService Bean을 생성한다.
↓
2. ScheduleController를 생성하려고 한다.
↓
3. 생성자를 확인한다.
↓
4. ScheduleService가 필요하다는 것을 확인한다.
↓
5. ScheduleService를 생성자에 전달하여
ScheduleController를 생성한다.
실제로 내부에서는 아래 코드와 비슷한 동작이 수행된다.
ScheduleController controller =
new ScheduleController(scheduleService);
우리가 직접 작성하지 않을 뿐, Spring이 대신 생성자를 호출해주는 것이다.
이것을 생성자 주입(Constructor Injection)이라고 한다.
객체를 생성할 때 필요한 객체를 생성자의 매개변수로 전달하는 방식을 생성자 주입(Constructor Injection)이라고 한다.
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
Spring에서는 여러 의존성 주입 방식이 존재하지만, 현재는 생성자 주입을 가장 권장한다.
생성자 주입을 권장하는 이유
예전에는 다음과 같은 필드 주입 방식을 많이 사용했다.
@Autowired
private ScheduleService scheduleService;
하지만 현재는 다음과 같은 방식이 가장 많이 사용된다.
private final ScheduleService scheduleService;
@RequiredArgsConstructor
그 이유는 다음과 같다.
- final을 사용할 수 있다.
- 객체 생성 시점에 반드시 의존성이 주입된다.
- 생성 이후에는 다른 객체로 변경할 수 없다.
- 테스트 코드 작성이 쉽다.
- 순환 참조 문제를 비교적 빠르게 발견할 수 있다.
즉 객체가 생성되는 순간부터 완전한 상태를 보장할 수 있다는 것이 가장 큰 장점이다.
정리
@RequiredArgsConstructor는 Spring에서 제공하는 기능이 아니다.
Lombok이 제공하는 기능이다.
하지만 Spring에서 가장 권장하는 생성자 주입 방식과 매우 잘 어울리기 때문에 Controller와 Service에서 거의 필수적으로 사용된다.
결국 @RequiredArgsConstructor는 단순히 생성자를 자동으로 만들어주는 어노테이션이 아니라, Spring의 생성자 주입을 간결하고 안전하게 구현하기 위한 Lombok의 도구라고 이해하면 된다.