Skip to main content

필드주입을 쓰면 안되는 이유를 아시는지요.

· 7 min read

레거시 코드를 마이그레이션 하던중 @Autowired을 통해 필드 주입으로만 의존성을 주입되어 있는 것을 발견하였다. 이를 생성자 주입으로 변경 하려고 하였지만 왜? 라는 의문이 들었고 이 포스팅 작성을 시작하였다.

의존성 주입

Spring Framework에서 의존성을 주입하는 방법은 3가지가 있다.

  1. 생성자 주입(Constructor Injection)
  2. 필드 주입(Field Injection)
  3. 수정자 주입(Method-Setter Injection)

그럼 이중 어떤 방법이 가장 권장될까?
결론이 생성자 주입 이라는 것은 아마 스프링 사용자라면 대부분 알고있다. 하지만 왜 생성자 주입이 가장 권장되는지 정확히 알고있지 못하다. 지금부터 이부분에 대해서 파악해보자.

1. 생성자 주입

@Component
public class OrderService {

private final PaymentService paymentService;

@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}

public void processOrder() {
paymentService.pay();
}
}

2. 필드 주입

@Component
public class OrderService {

@Autowired
private PaymentService paymentService;

public void processOrderPay() {
paymentService.pay();
paymentService.pay();
}
}

3. 수정자 주입 (Method - Setter 주입)

@Component
public class OrderService {

private PaymentService paymentService;

@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}

public void processOrder() {
paymentService.pay();
}
}

생성자 주입을 사용해야 하는 이유

생성자 주입 사용을 권하는 이유는 다음과 같다.

  1. 순환 참조 방지
  2. final 선언이 가능
  3. 테스트 코드 작성 용이

순환 참조 방지

객체에 의존성을 추가 하게 되면 순환 참조 문제가 발생한다. 즉 A가 B를 참조, B가 A를 참조 와 같은 경우가 발생하는 것이다. 예를 한번 들어보자.

필드 주입을 통해서 순환 참조를 구성해보자. Order / Payment Bean을 생성하고 서로 필드 주입을 진행한다.

@Component
public class OrderService {

@Autowired
private PaymentService paymentService;

public void processPay() {
paymentService.pay();
}
}

@Component
public class PaymentService {

@Autowired
private OrderService orderService;

public void processOrder() {
orderService.order();
}
}

그리고 이 두개의 Bean을 주입해보자.

@Component
public class OrderPayService {

@Autowired
private OrderSerivce orderService;

@Autowired
private PaymentService paymentService;

public void processPayOrder() {
orderService.order();
paymentService.pay();
}
}

그리고 해당 서버를 구동하면, 정상적으로 동작을 한다, 하지만 processPayOrder()를 호출하게 되면 순환참조로 인해 서버가 죽는 상황이 발생한다.

결과를 보면 메소드 실행 전까지 순환참조가 있더라도 해당 문제를 빌드 시점에서 알수없다. 이 예제를 생성자 주입을 통해 진행하면 서버 실행 시점에서 바로 에러를 catch할수있다.

@Component
@RequiredArgsConstructor
public class OrderService {

private final PaymentService paymentService;

public void processPay() {
paymentService.pay();
}
}

@Component
@RequiredArgsConstructor
public class PaymentService {

private final OrderSerivce orderService;

public void processOrder() {
orderService.order();
}
}

@Component
@RequiredArgsConstructor
public class OrderPayService {

private final PaymentService paymentService;
private final OrderSerivce orderService;

public void processPayOrder() {
orderService.order();
paymentService.pay();
}
}

서버자체가 구동되지 않아 순환참조를 빌드 시점에서 방지 가능하다.

에러

에러

이런차이가 발생하는 이유?

  • 필드 주입, 수정자 주입은 빈을 생성한후, 주입하려는 빈을 찾아 주입
  • 생성자 주입은 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 생성됨, 그리고 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출, 즉 먼저 빈을 생성하지 않고 주입하려는 빈을 찾음
  • 해당 이유로 객체 생성 시점에서 빈을 주입하기 때문에, 서로 참조하는 객체가 생성되지 않은 상태에서 그 빈을 참조하기 때문에 오류가 발생

final 선언 가능

필드 주입과 수정자 주입을 통해서는 주입하려는 필드를 final 로 선언이 불가능 하다. 즉, 추후에 해당 값이 변경될 수도 있다는 의미이다.

생성자 주입은 필드를 final로 선언이 가능하며, 런타임 시점에 객체의 불변성을 보장한다.

테스트 코드 작성 용이

생성자 주입을 사용하면 스프링 컨테이너의 도움 없이 테스트 코드를 편리하게 작성이 가능하다. 테스트 하고자 하는 클래스에 필드 주입이나 수정자 주입으로 Bean이 주입되면, Mockito 와 같은 목킹을 하여 테스트를 진행해야한다. 하지만 생성자 주입을 사용하면 단순하게 원하는 객체를 생성하여, 생성자에 넣어줌으로써 사용이 가능하다.

필드 주입의 경우

@Autowired
private PaymentService paymentService;

@Test
void test() {
OrderService orderService = new OrderService();
orderService.processOrder(); // 👉 NPE 발생, 의존성 주입 안 됨
}

생성자 주입의 경우

class OrderServiceTest {

@Test
void processOrder() {
// 👇 목 객체 직접 생성
PaymentService fakePaymentService = new PaymentService() {
@Override
public String pay() {
return "테스트 결제 완료";
}
};

// 👇 생성자에 주입
OrderService orderService = new OrderService(fakePaymentService);
String result = orderService.processOrder();
assertThat(result).isEqualTo("테스트 결제 완료");
}
}

이렇게 생성자 주입의 장점에 대해서 알아보았다. 어렴풋이 알고있었던 부분에 대해서 깊이 파고들어 확인 해보니, 좀더 많은 내용을 알수있는 좋은 경험 이였다.