웹 개발

spring에서 autowired 대신 생성자 주입을 사용해야 하는 이유

노루아부지 2021. 5. 12. 21:22

 

Spring에서 의존관계 주입을 받을 때는 당연하게 아래와 같이 인스턴스 변수에 @Autowired@Inject를 사용해 왔습니다.

public class ExampleController {
  @Autowired
  private UserService userService;
}

 

그러다가 Intellij로 프로젝트를 옮겨서 실행해 보니 Intellij에서 아래와 같은 경고 메시지가 발생했습니다.

 

Field injection is not recommended

즉, Field 주입이 권장되지 않는다는 메시지가 발생했습니다.

 

 

 

의존관계 주입(Dependency Injection, DI)

이 메시지를 이해하기 위해서는 우선 주입이 무엇인지 알아야 합니다.

 

먼저 의존관계는 항상 방향성이 있습니다.

예를 들어, A가 B에 의존하고 있을 때는 A->B라고 표시할 수 있습니다. 즉, B가 변경되면 A도 영향을 미친다는 의미입니다.

 

 

강한 결합

 

객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조입니다. A 클래스 내부에서 B 객체를 직접 생성하고 있다면 B객체를 C 객체로 바꾸고 싶을 경우에 A 클래스도 수정해야 하는데, 이것을 강한 결합이라 합니다.

 

 

느슨한 결합

 

객체를 주입받는 것은 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는 것입니다. 이렇게 하면 결합도를 낮출 수 있습니다.

 

 

 

권장되는 DI

Spring 공식 문서에서는 두 개의 권장된 DI를 알려줍니다.

  1. Constructor-based dependency injection (생성자 기반 주입)
  2. Setter-based dependency injection (Setter 기반 주입)

 

 

Setter-based dependency injection

 

다음은 이해를 돕기위해 spring을 사용하지 않은 코드입니다.

 

public interface Service {
  void doSomething();
}
public class ServiceImpl implements Service {
  @Override
  public void doSomething() {

  }
}
public class Controller {
  private Service service;

  public void setService(Service service) {
    this.service = service;
  }
  
  public void callService() {
    service.doSomething();
  }
}
public class Test {
  public static void main(String [] args) {
    Controller controller = new Controller();
    controller.setService(new ServiceImpl());
  }
}

 

  • Service는 인터페이스이고 인터페이스는 인스턴스화 할 수 없으므로 인터페이스의 구현체(ServiceImpl)가 필요하다.
  • Controller는 setter를 통해 주입된 ServiceImpl의 내부 동작을 아무것도 알지 못하고 알 필요도 없다.

setter 주입의 문제는 수정자를 통해서 Service의 구현체를 주입해주지 않아도 Controller 객체가 생성 가능하다는 것입니다.

Controller 객체가 생성 가능하다는 것은 내부에 있는 callService()도 호출 가능하다는 것인데, callService() 메서드는 service.doSomething() 메서드를 호출하고 있으므로 NullPointerException이 발생합니다.

 

 

Constructor-based dependency injection

 

Controller의 setter를 삭제하고, 생성자를 추가합니다.

public class Controller {
  private Service service;

  public Controller(Service service) {
    this.service = service;
  }

  public void callService() {
    service.doSomething();
  }
}
public class Test {
  public static void main(String [] args) {
    Controller controller = new Controller(new ServiceImpl());
  }
}

 

생성자 주입을 사용하면 다음과 같은 이점이 있습니다.

  • null을 주입하지 않는 한 NullPointerException은 발생하지 않는다.
  • 의존관계 주입을 하지 않은 경우에는 Controller 객체를 생성할 수 없기 때문에, 컴파일 단계에서 오류를 잡아낼 수 있다.
  • final을 사용할 수 있다. final로 선언된 레퍼런스 타입 변수는 반드시 선언과 함께 초기화가 되어야 하므로 setter 주입 시에는 의존관계 주입을 받을 필드에 final을 선언할 수 없다.
  • final의 장점은 누군가가 Controller 내부에서 service 객체를 바꿔치기할 수 없다는 점이다.
  • 순환참조를 사전에 방지할 수 있습니다.
    • A가 B를 참조하고, B가 A를 참조할 경우 서로 참조를 계속하다가 결국 StackOverflowError를 발생시키고 종료됩니다.
    • Field Injection이나, Setter Injection은 객체 생성 후 값이 주입되는 것이기 때문에 실제 프로그램이 실행되기 전까지는 순환 참조가 되고 있는지 알 수 없습니다.
    • Constructor Injection의 경우에는 순환 참조가 발생하면 컴파일이 되지 않습니다. 이는 컨테이너가 빈을 생성하는 시점에서 객체 생성에 사이클 관계가 생기기 때문입니다.

 

 

 

Field Injection 대신 Constructor Injection을 사용해야 하는 이유

 

1. 단일 책임 원칙 관점

field injection은 @Autowired만 선언하면 되기 때문에 간단합니다. 하지만, 의존하는 객체가 많다는 것은 하나의 클래스가 많은 책임을 가진다는 의미입니다.

상대적으로 Constructor Injection을 사용할 때는 Constructor 매개 변수가 많아지면서 잘못되어간다는 느낌을 받기 쉽게 때문에 리팩토링을 필요로 한다는 좋은 지표가 될 수 있다........ 고 하는데

 

사실 lombok을 사용하면 Constructor Injection도 어노테이션 하나(@RequiredArgsConstructor)로 쉽게 만들 수 있기 때문에 현재로서는 별 의미 없는 장점인 것 같습니다.

 

2. Field에 final 키워드 사용

Constructor Injection을 사용하면 final 키워드를 사용할 수 있습니다.

 

3. 순환 참조 방지

Field Injection이나, Setter Injection은 객체 생성 후 값이 주입되는 것이기 때문에 실제 프로그램이 실행되기 전까지는 순환 참조가 되고 있는지 알 수 없습니다.

Constructor Injection의 경우에는 순환 참조가 발생하면 컴파일이 되지 않습니다. 이는 컨테이너가 빈을 생성하는 시점에서 객체 생성에 사이클 관계가 생기기 때문입니다.

 

 

4.  결합도가 낮기 때문에 테스트가 쉽다

Field Injection은 Spring Container를 제외하면 주입을 할 방법이 없기 때문에 JUnit 등을 이용해 테스트하려면 까다로운 설정 과정을 거쳐야 합니다.

하지만 Constructor Injection을 사용하면 Spring Container의 도움 없이 간단하게 테스트를 할 수 있습니다.

 

 

 

 

 

 

참고한 자료

yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

studiou.tistory.com/9#constructor-based-dependency-inejction

velog.io/@dahye4321/Constructor-%EC%A3%BC%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

 

 

728x90
loading