원래는 이전 포스트에서 작성하던 부분이었으나 너무 글이 길어지는 관계로 따로 분리하였다.
생성자 주입
위의 포스트에서는 의존성 주입 방법으로 필드 주입, Setter 메서드 주입, 생성자 주입 세 가지 방법을 제시했다. 그중 마지막 방법인 생성자 주입은 말 그대로 객체의 생성자에서 의존하는 Bean 객체를 파라미터로 넘겨받는 방식이다.
@Component
public class Greeter {
private Prohibiter prohibiter;
@Autowired
public Greeter(Prohibiter prohibiter) {
this.prohibiter = prohibiter;
}
...
최근에는 필드 주입, Setter 메서드 주입보다 이 생성자 주입 방식을 많이 활용한다고 한다. 왜일까?
생성자 주입의 장점
생성자 주입이 좋은 이유는 여러 가지가 있겠지만 크게 다음과 같다.
-
테스트에 용이함.
-
의존성의 크기를 체감할 수 있음.
-
순환 참조 문제를 해결할 수 있음.
테스트에 용이하다는 건 무슨 뜻일까?
@Component
public class Greeter {
@Autowired
private Prohibiter prohibiter;
...
만약 위처럼 필드 주입 방식을 사용한다면 테스트 코드를 작성할 때 우리는 이 필드를 초기화시킬 수 없는 문제가 발생한다. 왜냐면 저 필드에 의존성을 주입해주던 건 개발자가 아닌 프레임워크가 하던 일이기 때문이다. 그리고 테스트 대상 객체가 초기화되었을 때 어떤 문제가 있어서 또는 단순 누락으로 해당 필드가 초기화되지 못했다면 null을 참조하고 있기 때문에 NullPointerException을 발생시킬 수 있는 등 여러 가지 단점이 있다.
@Component
public class Greeter {
private Prohibiter prohibiter;
@Autowired
public Greeter(Prohibiter prohibiter) {
this.prohibiter = prohibiter;
}
...
그러나 생성자 주입 방식을 사용한다면 테스트 코드를 작성할 때 테스트 대상 객체를 생성하는 동시에 내부 필드에 의존성을 주입해줄 수 있기 때문에 이런 문제가 발생하지 않는다. 생성자 함수 특성상 요구되는 매개변수, 즉 의존성을 주입해주지 않으면 객체를 생성할 수 없기 때문이다. 스프링 컨테이너는 생성자 매개변수로 요구되는 의존성을 확실하게 모두 주입해주기 때문에 NullPointerException이 발생할 가능성을 훨씬 줄일 수 있다.
의존성의 크기를 체감할 수 있다는 건 무슨 뜻일까?
@Autowired
public Greeter(Prohibiter prohibiter, Validator validator, Member member, Options options) {
...
}
이는 어떤 기술적인 의미보다는 코드 작성 그 자체에 관한 것이다. 생성자 주입 방식을 사용할 때는 의존성이 늘어날 때마다 생성자의 매개변수가 점점 늘어난다. 그러면 코드를 작성하는 점점 늘어나는 생성자 매개변수를 보면서 뭔가 의존성이 지나치게 많아지고 있다는 것을 알아챌 수 있는 것이다. 그래서 클래스 간 의존성을 검토하거나 리팩터링을 적용하는 등 코드의 품질을 높이는 계기가 될 수 있다.
그렇다면 순환 참조 문제는 무엇일까? 이는 객체 A에서 B를 의존하고 객체 B에서 A를 의존하는, 즉 서로 꼬리를 무는 의존 관계에서 발생할 수 있는 문제다.
@Component
public class Greeter implements InitializingBean, DisposableBean{
private String format;
@Autowired private Prohibiter prohibiter;
public String printProhibiter(){
return "Prohibiter is " + prohibiter.printGreeter();
}
...
@Component
public class Prohibiter {
@Autowired private Greeter greeter;
public String printGreeter(){
return "Greeter is " + greeter.printProhibiter();
}
...
올바른 프로그램 디자인이라면 이런 문제가 발생하지 않겠지만 개발 중 실수나 다른 원인에 의해 발생할 수 있다. 위의 코드를 보면 Greeter 클래스에서는 printProhibiter 메서드를 호출하여 Prohibiter 클래스를 의존하고 있다. 그런데 Prohibiter 클래스의 printProhibiter 메서드는 printGreeter 메서드를 호출하여 다시 Greeter 클래스를 의존하고 있다. 이처럼 서로 무한정 참조하는 문제를 순환 참조라 한다.
하지만 프로그램을 빌드해서 실행하기 전까지는 발생하지 않는 Unchecked Exception이기 때문에 만약 대규모의 복잡한 프로그램이라면 모르고 지나칠 수 있다. 실제로 위의 코드는 "Constructed with Interface"라는 생명주기 메서드의 문자열이 출력된 후 에러가 발생한 것을 볼 수 있다. 그러나 스프링에서 생성자 주입 방식을 활용하면 이와 같은 사태를 예방할 수 있다.
@Component
public class Greeter implements InitializingBean, DisposableBean{
private String format;
private Prohibiter prohibiter;
@Autowired
public Greeter(Prohibiter prohibiter) {
this.prohibiter = prohibiter;
}
...
@Component
public class Prohibiter {
private Greeter greeter;
@Autowired
public Prohibiter(Greeter greeter) {
this.greeter = greeter;
}
...
생성자 주입 방식에서는 프로그램이 실행되기 전에 순환 참조(circular reference)가 발생하는 것을 탐지하여 UnsatisfiedDependencyException을 발생시키기 때문에 위처럼 실제로 애플리케이션이 실행되기 전에 문제를 확인할 수 있다. 왜 생성자 주입 방식에서는 다른 방식에서는 검출하지 못하는 이 문제를 검출할 수 있는 걸까?
이 문서에 따르면 필드, Setter 메서드 주입 방식에서는 의존 Bean 객체를 생성한 후 필드에 주입하거나 Setter 메서드를 호출하여 주입한다고 한다. 그러나 생성자 주입 방식에서는 생성자로 객체를 생성하는 시점에 의존하는 Bean 객체를 찾거나 생성하여 주입한다. 만약 위처럼 생성되지 않은 두 객체가 서로를 참조, 생성하려고 한다면 아직 의존하는 객체가 완전히 생성되지 않았기 때문에 BeanCurrentlyInCreationException가 발생하여 순환 참조 문제를 탐지할 수 있는 것이다.
이런저런 강의를 듣고 책을 읽어보면서 세 가지 방법 중 어떤 걸 사용해야 할지 고민이 좀 됐는데 앞으로는 생성자 주입 방식으로 진행해야겠다.
[참고 | www.baeldung.com/constructor-injection-in-spring]
[참고 | madplay.github.io/post/why-constructor-injection-is-better-than-field-injection]
'Spring 프레임워크 > 이론' 카테고리의 다른 글
Spring의 AOP(Aspect Oriented Programming) (0) | 2021.02.26 |
---|---|
Spring의 빈 범위(Bean Scope) (0) | 2021.02.23 |
Spring의 빈 생명주기(Bean Lifecycle) (0) | 2021.02.22 |
Spring의 제어 역전 컨테이너(Inversion of Control Container) (0) | 2021.02.22 |
Spring의 컴포넌트 스캔(Component Scan) (0) | 2021.02.21 |