[Spring] 필드주입방식이 권고되지 않는 이유
https://lordofkangs.tistory.com/406
지난 포스팅에서 의존성 주입에 대해서 알아보았다. 의존성 주입(DI)은 세가지 방식으로 이루어진다.
1) 생성자 주입방식 ( final )
2) 필드 주입방식 ( @Autowired )
3) 수정자 주입방식 ( Setter )
세가지 방식 중 '필드주입방식'은 권고되지 않는다. 권고되지 않는 이유를 생성자,수정자 주입방식과 비교하여 알아보자.
1. 순환참조를 예방할 수 없다.
순환참조란 객체A가 객체B의 의존하는데, 객체B도 객체A에 의존하는 구조이다. 이런 구조는 참조가 참조를 만들고 참조가 또 참조를 만드는 참조의 무한루프를 만든다. 순환참조구조는 설계되지 않는 것이 중요하지만 시스템이 비대해지면 순환참조구조가 만들어 질 수 있다. 그래서 사전에 예방하는 것이 중요한데, 필드주입방식은 사전예방이 어렵다.
Spring 컨테이너는 Bean을 갖는다. 필드주입방식과 수정자 주입방식은 스프링 컨테이너에 Bean 생성되고 컨테이너가 초기화된 이후에 의존성 주입(DI)이 이루어진다. 그래서 순환참조 위험이 있는데도 탐지하지 못하고 어플리케이션이 실행될 수 있다.
ServiceA 클래스 ( 필드주입방식 )
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void run() {
serviceB.run();
}
}
ServiceB 클래스 ( 필드주입방식 )
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
public void run(){
serviceA.run();
}
}
ServiceA와 ServiceB는 순환참조관계이다. ServiceA와 ServiceB가 Bean으로 생성될 때는 아무 문제가 없다. 스프링컨테이너에 Bean이 생성되고 초기화 된 후, 컴포넌트 스캔으로 @Autowired에 Bean 주입되어도 문제가 발생하지 않는다. 왜냐하면 순환참조는 메소드에서 발생하기 때문이다. 실제로 프로그램이 동작해야 문제가 발견되는 것이다. 이런 런타임 오류는 치명적이다.
만약 이를 생성자 주입방식으로 만들면 어떻게 될까?
생성자 주입은 생성자의 파라미터로 Bean을 주입하는 방식이다. 생성자이므로 스프링 컨테이너에 Bean을 생성할 때 Bean이 주입된다. 만약 주입할 Bean이 없다면 해당 Bean을 생성한다. 또 그 Bean이 없다면 다시 Bean을 생성한다. 이런 꼬리에 꼬리를 무는 방식이 순환참조를 발견한다.
ServiceA 클래스 ( 생성자주입방식 )
@Service
@Data
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB){
this.serviceB = serviceB;
}
}
ServiceB 클래스 ( 생성자주입방식 )
@Service
@Data
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA){
this.serviceA = serviceA;
}
}
스프링 프로젝트를 만들어서 실행하면 아래와 같은 결과가 나온다.
로그를 읽어보면, 어플리케이션 컨텍스트(스프링컨테이너) 안의 Bean 사이에서 의존관계가 싸이클(순환)을 형성한다고 나와있다. 이는 당연한 결과이다. ServiceA는 ServiceB가 필요하다. 그래서 ServieB를 생성하려니 ServiceA가 필요하다. 그럼 ServiceA를 생성하려니 다시 ServiceB가 필요하다. 이것이 반복되는 것이다.
이처럼 순환참조방식은 스프링 컨테이너가 로딩되는 시점에 오류를 발견할 수 있어서 치명적이지 않다.
2. 불변객체 사용이 불가능하다.
생성자 주입방식은 final 사용이 가능하다. final은 컴파일 시점에 상수로 값을 고정하는 키워드이다. 다시 말해서, 런타임 중에는 바뀌지 않음을 의미한다. 이런 점이 정적이라 안 좋아 보일 수 있다. 그러나 필수적인 의존관계에서 동적인 변함은 치명적일 수 있다. Spring에서는 싱글톤으로 Bean을 생성한다. Service Bean이 특정 Repository Bean을 참조하는데 외부요인이 다른 Repository 객체로 변경하면 바라보는 DB가 달라질 수 있다. 그러므로 필수적인 의존관계는 final로 고정해야 한다.
필드주입방식은 final로 고정할 수 없다. 앞서 말했듯, 객체가 생성되는 시점에 주입되는 방식이 아니라 객체가 생성된 후에 주입하는 방식이다. final로 고정되어 있으면 객체 생성후 주입할 수 없기 때문이다.
3. 의존성 숨김( Dependency Hiding ) 불가
필드주입방식은 필드에 의존성을 주입할 때 Reflection API를 사용한다.
생성자 주입방식은 Reflection API를 사용하지 않는다. Reflection API는 인자가 있는 생성자를 사용할 수 없기 때문이다. 생성자 주입방식이나 수정자 주입방식은 생성자(Constructor)나 수정자(Setter)와 같은 내부로직으로 의존관계를 주입한다. 다시말해, 데이터만 넘겨받고 내부로직에서 의존관계를 주입하는 방식이다. 반면, 필드주입방식은 Reflection API가 직접 필드에 접근하여 주입한다.
이는 의존성이 외부API로 노출됨을 의미한다.
이런 방식은 객체지향 프로그래밍 관점에서 좋지 못하다. 객체는 캡슐화가 되어 외부로부터 내부를 보호해야 한다. 필드주입방식은 외부가 내부에 직접 접근하는 방식으로 내부를 보호하지 못한다.
4. 테스트가 힘들다.
생성자 주입방식과 수정자 주입방식은 테스트 객체를 파라미터로 넘기면 된다. 한마디로 언제든 테스트 객체를 변경할 수 있다. 그러나 필드주입방식은 @Autowired로 필드로 직접 객체를 주입받는 방식이기 때문에 테스트 과정이 복잡해진다.
그럼 수정자 주입 방식은?
의존관계가 필수적인 경우 생성자주입방식을 사용하면 된다. 반면 의존관계가 선택적인 경우, 수정자 주입방식을 사용해야 한다. 번역기를 예를 들어보자. 번역기는 언어설정이 고정되어 있지 않다. 선택에 따라 한국어,영어,일본어,중국어.. 동적으로 바뀐다. 번역기 객체가 언어설정 객체를 의존한다면 언어설정 객체는 final로 고정되어 있으면 안 된다. 언제든 설정에 따라 바뀌어야 한다.
public class Translator {
private LanguageSetting languageSetting;
@Autowired // Setter 수정자 주입
public void setLanguageSettings(LanguageSetting languageSetting) {
this.languageSetting = languageSetting;
}
public String translate(String text) {
return languageSettings.translate(text) ;
}
}
이처럼 동적으로 의존관계가 변해야 하는 경우, 수정자 주입방식으로 의존관계를 주입하면 된다.
필드주입방식은 필수의존관계를 주입하기에는 final을 사용 못하고 선택적으로 의존관계를 주입하기에는 객체 주입이 힘들다. 또한 순환참조를 발견할 수도 없고 객체지향 프로그래밍 관점에도 맞지 않다. 그러므로 필드주입방식의 사용은 권고되지 않는다.
참고자료