Constructor injection을 사용하자.

김다혜·2020년 6월 20일
2
post-thumbnail

GITHUB 참고 소스

👉 전체 코드는 여기에서 👈

field injection을 사용하다보면
Field injection is not recommended 이라는 문구를 본 경험이 있을 것이다.

상세 메세지
Field injection is not recommended
Inspection info: Spring Team recommends: "Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies".

field injection을 추천하지 않고, construction injection을 추천할까? 🤔

의존성 주입의 3가지 방법

의존성 주입 방식에는 대표적으로 3가지가 있다.

1. 필드 주입 (Field Injection)

@Component
public class Juice {
    @Autowired
    private Orange orange;
}

2. 세터 주입 (Setter Injection)

@Component
public class Milk {
    private Cereal cereal;

    @Autowired
    public void setCereal(Cereal cereal) {
        this.cereal = cereal;
    }
}

3. 생성자 주입 (Constructor Injection)

@Component
public class Water {
    private final TeaBag teaBag;

    public Water(TeaBag chamomile) {
        this.teaBag = chamomile;
    }
}

field injection 대신 constructor injection을 사용해야하는 이유

1. 단일 책임 원칙 관점

field injection으로 의존성을 추가하기에 굉장히 간단하다. (클래스에 @Autowired를 선언하고, 필드만 추가하면 끝이다!) 하지만, 의존하는 객체가 많다는 것은, 하나의 클래스가 많은 책임을 가진다는 의미이다.

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

2. 필드에 final 키워드 사용

field injection을 사용할 때에는 final 키워드를 사용할 수 없지만,
constructor injection을 사용할 때에는 final 키워드를 사용할 수 있다.

그렇기 때문에 immutable하게(변경 불가능하게) 사용할 수 있다.

3. 순환 참조 방지

객체의 의존성을 추가하다보면 흔히(?) 발생할 수 있는 문제 중 하나가, 순환 참조이다.
순환 참조는 A -> B를 참조하면서, B -> A를 참조하는 경우 발생하는 문제이다.
constructor injection을 사용하면, 아래와 같이 어플리케이션 실행 시점에 순환 참조를 확인할 수 있다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  a defined in file [...\injection\constructor\A.class]
↑     ↓
|  b defined in file [...\injection\constructor\B.class]
└─────┘
  • field injection, setter injection에서는 메서드 실행 시점에만 발견할 수 있다.

4. DI 컨테이너와 결합도가 낮기 때문에 테스트하기 좋다. 👍

이 내용은 개인적으로 굉장히 좋은 장점이라고 생각된다.

결론부터 말하자면, 스프링 컨테이너의 도움 없이, 우리는 간단하게 테스트를 할 수 있다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringInjectionApplication.class)
class ConstructorInjectionTest {
    @Autowired
    private Water water;

    @Test
    @DisplayName("생성자 주입 테스트 코드")
    void constructor_injection() {
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(Chamomile.class);
    }
}

위와 같이 테스트할 수도 있지만, 아래와 같이 스프링 컨테이너의 도움 없이 테스트를 할 수 있다. 해당 객체가 인터페이스를 파라미터로 받는다면, 휘뚜루마뚜루 객체를 변경하여 테스트를 할 수 있다는 말이다. 👍

class ConstructorInjectionTest {
    @Test
    @DisplayName("생성자 주입 사용 시, 스프링 컨테이너에 의존하지 않고 테스트할 수 있다.")
    void constructor_injection_without_dependence() {
        Water water = new Water(new GreenTea());
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(GreenTea.class);
    }
}

5. NullPointerException 방지

field injection을 사용하며, 테스트 코드를 짜 본 개발자라면 굉장히 공감할 것 같은 내용이라고 생각한다.

field injection, setter injection은 new 키워드를 사용하여 객체를 생성한다면, 필수 협력자가 셋팅되지 않은 객체를 사용하려고 할때, NullPointerException이 발생한다.

하지만, constructor injection을 사용한다면, 필수 협력자가 셋팅되지 않은 상태에서는 생성조차 하지 못하게 된다.(필드 셋팅을 강제한다.) 따라서, NullPointerException은 발생하지 않을 것이다.

constructor injection Too Much Info

1. 매개변수가 빈이 아닌 경우, 컴파일 에러가 발생한다.

@Component
public class Water {
    private final TeaBag teaBag;
    private final Kettle kettle;

    public Water(TeaBag chamomile, Kettle kettle) { // Kettle이 빈이 아닌 경우 컴파일 에러 발생
        this.teaBag = chamomile;
        this.kettle = kettle;
    }
}

2. 매개변수가 interface 일 때

매개변수가 interface라면 아래의 2가지 방법으로 주입가능하다.

  • 매개변수 명을 구현체의 클래스 명과 일치시키기
  • @Qualifier 어노테이션 사용하기
public Water(@Qualifier("chamomile") TeaBag teaBag) {
    this.teaBag = teaBag;
}

3. @RequiredArgsConstructor 사용하여 주입

롬복의 @RequiredArgsConstructor를 사용하면 final이나 @NotNull을 사용한 필드에 대한 생성자를 자동 생성해준다.

@Component
@RequiredArgsConstructor
public class WaterByLombok {
    private final TeaBag greenTea;
}

위와 같이 생성자 주입을 할 수 있다 🙂
(편해지는만큼, 1. 단일 책임 원칙 관점에서 말한 생성자 주입의 장점은 다시 사라질 것 같다..😅)

결론

생성자 주입을 사용하자!

참고

2개의 댓글

"매개변수가 빈이 아닌 경우, 컴파일 에러가 발생한다."
매개변수가 Bean이 아니더라도 컴파일 에러가 발생하지 않는데 특정버전에 한정한 이야기 일까요?
4.3.17.RELEASE 에서 매개변수가 빈이 아니지만, 생성자의 인자로 설정했을 때 컴퍼일 에러가 발생하지 않았습니다.

1개의 답글