field injection is not recommended
?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가지가 있다.
@Component
public class Juice {
@Autowired
private Orange orange;
}
@Component
public class Milk {
private Cereal cereal;
@Autowired
public void setCereal(Cereal cereal) {
this.cereal = cereal;
}
}
@Component
public class Water {
private final TeaBag teaBag;
public Water(TeaBag chamomile) {
this.teaBag = chamomile;
}
}
@Autowired
어노테이션을 생략 할 수 있다.@Autowired
어노테이션을 붙여주어야 한다.@Autowired
를 붙일 수는 없다! only one!)field injection
으로 의존성을 추가하기에 굉장히 간단하다. (클래스에 @Autowired
를 선언하고, 필드만 추가하면 끝이다!) 하지만, 의존하는 객체가 많다는 것은, 하나의 클래스가 많은 책임을 가진다는 의미이다.
상대적으로 Constructor injection
을 사용할 때에는 Constructor
매개변수가 많아지면서 잘못되어간다는 느낌을 받기 쉽다. 즉, 리팩토링을 필요로 한다는 좋은 지표가 될 수 있다.
final
키워드 사용field injection
을 사용할 때에는 final
키워드를 사용할 수 없지만,
constructor injection
을 사용할 때에는 final
키워드를 사용할 수 있다.
그렇기 때문에 immutable하게(변경 불가능하게) 사용할 수 있다.
객체의 의존성을 추가하다보면 흔히(?) 발생할 수 있는 문제 중 하나가, 순환 참조이다.
순환 참조는 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
에서는 메서드 실행 시점에만 발견할 수 있다.이 내용은 개인적으로 굉장히 좋은 장점이라고 생각된다.
결론부터 말하자면, 스프링 컨테이너의 도움 없이, 우리는 간단하게 테스트를 할 수 있다.
@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);
}
}
field injection
을 사용하며, 테스트 코드를 짜 본 개발자라면 굉장히 공감할 것 같은 내용이라고 생각한다.
field injection
, setter injection
은 new 키워드를 사용하여 객체를 생성한다면, 필수 협력자가 셋팅되지 않은 객체를 사용하려고 할때, NullPointerException
이 발생한다.
하지만, constructor injection
을 사용한다면, 필수 협력자가 셋팅되지 않은 상태에서는 생성조차 하지 못하게 된다.(필드 셋팅을 강제한다.) 따라서, NullPointerException
은 발생하지 않을 것이다.
constructor injection
Too Much Info@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;
}
}
매개변수가 interface
라면 아래의 2가지 방법으로 주입가능하다.
@Qualifier
어노테이션 사용하기public Water(@Qualifier("chamomile") TeaBag teaBag) {
this.teaBag = teaBag;
}
@RequiredArgsConstructor
사용하여 주입롬복의 @RequiredArgsConstructor
를 사용하면 final
이나 @NotNull
을 사용한 필드에 대한 생성자를 자동 생성해준다.
@Component
@RequiredArgsConstructor
public class WaterByLombok {
private final TeaBag greenTea;
}
위와 같이 생성자 주입을 할 수 있다 🙂
(편해지는만큼, 1. 단일 책임 원칙 관점에서 말한 생성자 주입의 장점은 다시 사라질 것 같다..😅)
생성자 주입을 사용하자!
"매개변수가 빈이 아닌 경우, 컴파일 에러가 발생한다."
매개변수가 Bean이 아니더라도 컴파일 에러가 발생하지 않는데 특정버전에 한정한 이야기 일까요?
4.3.17.RELEASE 에서 매개변수가 빈이 아니지만, 생성자의 인자로 설정했을 때 컴퍼일 에러가 발생하지 않았습니다.