IntelliJ를 사용하여 Spring Boot 프로젝트를 진행하다가 의존성을 주입하는 과정에서 필드에 @Autowired를 사용하면 다음과 같은 메세지를 만나게 된다.
Field injection is not recommended
무슨 오류가 발생하거나 하지는 않지만 밑줄이 신경쓰인다. 신경은 쓰이지만 그냥 사용을 했었는데, 이번에 인턴을 진행하면서 이에 대해 지적을 받았고, 생성자 주입에 대해 공부하고 도입을 하였다.
아래 나오는 이야기는 모두 미묘하게 틀릴 수 있음, 내가 이해한 내용이고, 나중에 더 공부하고 나면 수정하지 않을까?
필드 주입은 다음과 같이 코드를 작성한다.
@Service
public class CouponService {
@Autowired
private OrdersService OrdersService;
이런 식으로 어떤 클래스의 필드에 @Autowired를 붙여주면 해당 필드에 맞는 빈을 컨테이너에서 찾아 의존성을 주입해주는 것으로 보인다. 아직 자바 알못이라 확실하진 않음..
지금 CouponService에 @Service가 붙어 있기 때문에(타고 들어가보면 @Component가 있음) 애플리케이션이 구동될 때 CouponService를 스프링 빈으로 만들게 되는데, CouponService의 private OrdersService ordersService라는 필드가 빈을 만듦에 있어서 반드시 초기화되어야(값이 존재해야) 하는 것은 아니기 때문에 우선 이런 요소들이 비어있는 빈을 만든 후에 의존성을 찾아서 주입한다고 이해했다.
다시 말해서, 애플리케이션이 구동될 때 빈을 만들게 되고, 빈에서 필요로 하는 의존성이 있으면 그걸 다시 컨테이너에서 찾겠지만, 그게 없더라도 크게 개의치 않고 우선 빈을 만든 후에 나중에 의존성을 주입해 준다는 이야기이다.
생성자 주입은 원래는 아래와 같이 한다.
@Service // OrdersService는 애플리케이션 구동 시점에 루트 컨테이너에 빈으로 등록됨
public class OrdersService {
private final OrdersRepository ordersRepository;
private final UserService userService;
@Autowired
public OrdersService(OrdersRepository ordersRepository, UserService userService) {
this.ordersRepository = ordersRepository; // @Autowired가 찾아준 빈을 넣어줌
this.userService = userService;
}
위 코드에서 OrdersService는 생성자의 인자로 두 개의 의존성을 필요로 한다. 여기에 @Autowired를 붙여주면 생성자의 인자를 컨테이너에서 찾아서 의존성을 주입하게 될 것이다.
이 생성자 주입과 필드 주입의 차이점을 이야기해보자면..
필드 주입은 우선 OrdersService의 빈을 만든 후에 필드의 의존성 주입을 하고, 생성자 주입은 OrdersService를 빈으로 만드는 과정에서 의존성 주입을 하게 된다. 즉, OrderService의 생성자의 인자도 컨테이너에서 찾아보게 되고, 없으면 만들어주면서, 인자들을 다 찾아 주입을 한 후에야 빈 생성이 완료되는 것이다.
이 과정을 lombok을 사용하면 더욱 간소화 할 수 있다.
@RequiredArgsConstructor
@Service
public class OrdersService {
private final OrdersRepository ordersRepository;
private final UserService userService;
@RequiredArgsConstructor는 선언한 필드 중 final이나 @NonNull이 붙어있는 필드의 의존성을 찾아서 주입해주는 역할을 한다.
밑에서 this.~ = ~ 같은 반복 코드를 이제는 작성하지 않아도 된다.
어쨋든 이렇게 생성자 주입을 사용하게 되면 세 가지 이점이 있다고 한다. 우선 final을 사용할 수 있어 의존성 주입한 객체의 변이를 막을 수가 있고, 다음으로 의존성 주입이 빈 생성 시점에 이루어지기 때문에 순환 참조가 있을 경우 애플리케이션을 구동할 때 미리 발견할 수 있다.
이게 무슨 소리냐 하면..
예를 들어서, 아래 코드처럼 OrdersService에서 UserService를 필요로 하고, UserService에서 OrdersService를 필요로 한다면?
@RequiredArgsConstructor
@Service
public class OrdersService {
private final OrdersRepository ordersRepository;
private final UserService userService;
@RequiredArgsConstructor
@Service
public class UserService {
private final OrdersService ordersService;
private final UserRepository userRepository;
애플리케이션 구동 시점에 최초로 OrdersService를 빈으로 만드는 과정에서 UserService의 빈을 필요로 하고, 해당 빈이 아직 없을 것이므로(OrdersService가 아직 없는데 UesrService를 만들 수 있을리가 없다) 빈팩토리에서 UserService를 빈으로 만들려고 할 것이다. 그러면 UserService에서는 OrdersService를 필요로 하고, OrdersService의 빈이 아직 만들어지지 않았으므로 빈 팩토리에서 OrdersService를 빈으로 만들려고 할 것이다. 그러면 다시 UserService를 빈으로 만들려고 할 것이고, 다시 OrdersService를 만들려고 할 것이고, ...
이렇게 순환 참조 문제가 발생하여 에러가 발생한다.
필드 주입을 사용하면 애플리케이션 자체는 문제 없이 구동(빈이 다 만들어짐)되기 때문에 순환 참조 오류가 있을 경우 사용자가 애플리케이션을 사용하는 도중에 이를 발견하게 될 것이다.
이는 개발이나 서버 운영에 있어서 굉장히 큰 이점으로, 오류를 서버를 실행시킨 후에 어떤 동작에 의해서 발견하게 된다면 서버가 이미 다운되어버리는 불상사가 생기게 될 것이다.
그 외에도 테스트 코드 작성에 유리하다고 하는데, 테스트 코드에 대해서는 잘 몰라서 뭐라고 적을 수가 없다!!
앞으로 필드 주입은 사용을 지양하자.