필드 주입을 피하자

의존관계 주입(Dependency Injection) 방법은 생성자 주입, 수정자 주입, 필드 주입이 있다. 왜 필드 주입 사용을 줄이고, 생성자/수정자 주입을 사용해야 하는지 알아보자.

필드(Field) 주

먼저 필드 주입의 경우, 코드량이 줄어드는 장점( boilerplate 제거)이 있다. 그리고 그게 전부이며, 단점은 뼈아프다.

@Autowired
private ServiceA serviceA;
@Autowired
private ServiceB serviceB;
@Autowired
private ServiceC serviceC;

첫째, DI 컨테이너와 결합이 매우 강하게 되어 외부에서 사용하기 어려워 진다.

DI 프레임 워크의 핵심 아이디어 중 하나는 ‘관리되는 클래스가 DI 컨테이너에 의존하지 않아야 한다’ 이다. 즉, 모든 필수 종속성을 전달하면 독립적으로 인스턴스화 할 수있는 단순한 POJO 여야한다. 이렇게 하면 DI 컨테이너를 시작하지 않고 단위 테스트에서 이를 인스턴스화하고 별도로 테스트 할 수 있다. 하지만 필드 주입의 경우, 스프링 설정 파일을 읽고 모든 Bean 설정이 되어야 테스트를 할 수 있다.

둘째, 의존 관계가 보이지 않는다.

DI 컨테이너를 사용하면 클래스가 스스로 종속성을 관리 할 필요가 없음을 의미한다. 의존성을 얻기 위한 책임을 더 이상 클래스가 하지 않는다. DI 컨테이너 또는 단위 테스트의 경우, mock 객체를 직접 만드는 등과 같은 다른 무언가가 책임진다. 그래서 클래스가 더 이상 종속성을 가져올 책임이 없으면, 수정자 또는 생성자를 사용하여 클래스 의존관계를 명확하게 보여줘야 한다. 그러나 필드 주입은 클래스 의존관계가 명확하지 않다. 그래서 필수 또는 불변 프로퍼티 DI 라면 생성자 주입, 선택적이고 가변 프로퍼티 DI라면 수정자 메소드 주입을 사용해서 보다 정확하게 의존 관계를 설명해야 한다.

생성자/수정자 주입

수정자 주입은 프로퍼티마다 번거롭게 수정자 메소드를 추가해줘야 한다는 점과 수정자 주입을 이용하면 필수적으로 DI 되어야 할 항목을 빼먹을 수 있다는 단점이 있다. 그렇다고 생성자 주입을 이용해 한 번에 모든 프로퍼티 값을 다 넣도록 강제한다면 선택적으로 설정하는 유연성을 갖기 어렵다. 그리고 일단 오브젝트를 만들고 나서 설정 값을 넣어야 하는 경우에도 생성자 주입은 적절하지 않다.

그래서 이 두 가지를 혼용해서 사용한다. 필수적이며 불변 프로퍼티의 경우, 생성자 주입을 사용한다. 선택적이며 가변 프로퍼티의 경우, 수정자 주입을 사용한다.

생성자(Constructor) 주입

private ServiceA serviceA;
private ServiceB serviceB;
private ServiceC serviceC;

@Autowired
public HomeController(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC) {
    this.serviceA = serviceA;
    this.serviceB = serviceB;
    this.serviceC = serviceC;
}

수정자(Setter) 주입

private ServiceA serviceA;
private ServiceB serviceB;
private ServiceC serviceC;

@Autowired
public void setServiceA(ServiceA serviceA) {
    this.serviceA = serviceA;
}
@Autowired
public void setServiceB(ServiceB serviceB) {
    this.serviceB = serviceB;
}
@Autowired
public void setServiceC(ServiceC serviceC) {
    this.serviceC = serviceC;
}

수정자 주입을 사용해야 하는 경우가 많으면 어떻게 해야 할까?

선택적이며 가변 프로퍼티 DI가 매우 많아서 수정자 메소드가 늘어난다면 관리나 가독성에 불편함이 생길 수 있다. 이런 경우 일반 메소드를 사용하는 DI 방법이 있다. 생성자와 똑같은 형식(파라미터를 가진 메소드를 만들고 @Autowired를 붙여주면 된다.)의 일반 메소드지만, 객체 생성 후에 호출이 가능하며, 비슷한 목적의 프로퍼티만 묶어서 여러개로 만들어 차례로 호출할 수도 있다. 한 번에 여러 개의 객체를 DI할 수 있으므로 수정자보다 코드량도 줄고 깔끔해 진다. 단, 이렇게 만들어진 클래스는 XML을 통해서는 의존관계를 설정할 방법이 없다는 점에 주의하자.

생성자 주입에 파라미터가 많으면 어떻게 해야 할까?

생성자에 필요한 파라미터가 많으면 지저분해 보인다. 이 때문에 필드 주입을 사용하는 경우도 많다. 그러나 지저분한 생성자는 실제로 우리에게 좋은 것을 말해준다. 이는 나쁜 코드 냄새이며, 클래스 설계 원칙 중 단일 책임 원칙(SRP: Single-Responsibility Principle)을 어기고 있다는 신호다. 즉, 리팩토링을 시작할 때임을 알려준다.

마무리

  • 필수적으로 DI 되어야 하거나 불변 프로퍼티라면 생성자 주입을 사용하자.
  • 선택적이거나 변경 가능성이 있는 프로퍼티라면 수정자 주입을 사용하자.
  • 대부분의 경우 필드 주입은 피하자.
  • 수정자 주입이 많다면, 일반 메소드 주입을 고려해 보자.
  • 생성자에 필요한 파라미터가 많다면, 리팩토링을 고려해 보자.