Lee's Grow up

[Spring/Core] DI(의존성 주입)은 생성자 주입을 사용해라 본문

Spring/Spring

[Spring/Core] DI(의존성 주입)은 생성자 주입을 사용해라

효기로그 2020. 2. 28. 14:38
반응형

이번 내용은 스터디를 진행하는 도중에 누군가가 요즘은 필드 주입을 사용하지 않는데 왜 사용했느냐라는 질문에 대한 내용을 정리하기 위해 스프링 공식 사이트의 Spring Core의 내용을 참고해서 정리한 내용입니다.

사용 이유

아래는 Spring Document에 나와있는 원문입니다.

Constructor-based or setter-based DI?
Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Required annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

뭐 사실 저 원문을 보려고 오신 건 아닐 거라 믿기 때문에 포스팅이 길어질걸 대비해 옆으로 길게 내용을 작성합니다.
해당 내용을 간략하게 줄여보자면, 우선 필드 주입은 언급도 안 하고, ( 여태 사용하던 나는 무엇...)
Construcor Base Injection(생성자 기반 주입)과 Setter Base Injection(수정자를 통한 주입)의 차이점에 대해서 설명합니다. 여담으로 스프링 진영도 생성자 기반 주입을 옹호한다고 나와있네요.

둘의 차이점
  • 생성자 주입은 필수 종속성, 수정자는 선택적 종속성에 사용하라
  • 생성자 주입은 final 키워드를 사용할 수 있다.
  • 생성자 주입은 생성자 인수가 많아지면. 코드가 더러워 보이므로 리팩토링을 하게 된다. 라는 식으로 적혀 있는데 구글 번역기를 돌리는 거라 대충 느낌만 적었습니다.

종속성

말 그대로 객체가 다른 객체에 종속되어 있다는 말입니다. 간단한 예제를 보면

class Book{    
    JavaBook javaBook = new JavaBook();

    ...
}

class JavaBook{
    ...
}

위와 같은 구성일 때 JavaBook 클래스는 Book 클래스가 변경이 되어도 영향이 없다.
하지만 Book 클래스는 JavaBook 클래스가 변경되면 영향을 받게 된다 이러한 상태를 Book가 JavaBook에 종속되었다고 표현합니다.

간단하게 종속성에 대해서 알아봤고, 그럼 생성자 주입은 필수 종속성, 수정자 주입은 선택적 종속성에 사용하라는 게 무슨 말일까?
아래 코드를 예제로 보겠습니다. 해당 코드는 스프링 동작이 아닌 순수 자바 코드입니다.

Constructor Base

public class Controller {

    private Service service;

    public Controller(Service service) {
        this.service = service;
    }
}

public class Service { }

public class Main {
    public static void main(String[] args) {

        Controller controller = new Controller(new Service());
        Controller controller2 = new Controller(null);
    }
}

보시는 바와 같이 생성자를 통한 주입의 경우 해당 의존 객체를 필수로 주입해주지 않는 한 객체를 생성할 수 없게 됩니다.
즉, null을 직접적으로 주입하지 않는한 객체는 NullPointerException을 발생하지 않습니다. 추가로 의존 객체에 final 키워드를 사용할 수 있는 장점도 있습니다.

Setter Base

public class Controller {

    private Service service;

    public void setService(Service service) {
        this.service = service;
    }

    public void servicePrint(){
        service.print();
    }
}

public class Service { 
    public void print(){
        System.out.println("someting to do...");
    }
}

public class Main {
    public static void main(String[] args) {

        Controller controller = new Controller();
        controller.setService(new Service());
        controller.servicePrint();

        Controller controller2 = new Controller();
        controller2.servicePrint(); // NullPointerException
    }
}

반면 Setter 주입의 경우 객체 생성 시 의존관계를 주입해주지 않아도 객체 생성이 가능하게 됩니다.
그렇기 때문에 사용 시 NullPointerException이 발생할 수 있어 항상 객체의 참조가 null인지 확인해야 하는 로직이 추가로 필요하게 됩니다.

순환참조

간단하게 생성자 기반 주입과, 수정자 기반 주입에 대해서 알아봤습니다. 이제 마지막으로 순환 참조에 대한 내용입니다.
생성자를 통한 주입의 경우 순환 참조를 앱 구동 단계에서 스프링이 오류를 찾을 수 있고, 수정자 주입의 경우 런타임시, 해당 순환 참조가 되어있는 메소드를 호출하기 전까지 순환참조인지 알 수 있는 방법이 없습니다. 아래 예제를 보겠습니다.

@Service
@AllArgsConstructor
public class TestService1 {

    private TestService2 testService2; 
}

@Service
@AllArgsConstructor
public class TestService2 {
    private TestService1 testService1;
}

위와 같이 간단하게 생성자 기반 주입으로 서로 순환참조를 하도록 설정하고 스프링을 실행하면 아래와 같은 오류가 발생합니다.
Error creating bean with name 'testService1': Requested bean is currently in creation: Is there an unresolvable circular reference?

그러나 수정자의 경우 빈 생성 자체는 문제없이 됩니다.
위에서 설명한 바와 같이 서로 메소드가 순환을 참조하고 있든 아니든, 객체 생성 시엔 알 수 있는 방법이 없기 때문에 정상적으로 동작되는 것처럼 보이다 런타임 시에 오류가 발생하는 문제점이 있습니다.

정리

이렇게 스프링에서 의존성을 주입받는 방법 2가지를 비교해봤습니다. 내용을 간단하게 정리하면 아래와 같습니다.

  • 생성자 기반 주입의 경우 NullPointException을 방지할 수 있다.
  • 생성자 기반 주입의 경우 객체에 final 키워드를 사용할 수 있다.
  • 생성자 기반 주입의 경우 순환 참조를 앱 구동 시 검출할 수 있다.
  • 생성자 기반 주입의 경우 생성자의 인자가 많아지면 코드가 더러워져 리펙토링을 하게 된다.

위 4개의 장점이 Spring에 소개된 생성자 기반 주입의 장점입니다.
이렇게 장점이 많으니 앞으로 생성자 기반 주입만 사용해야겠습니다. 더불어 요즘은 lombok랑 연동해서 사용하면 어노 테이션 하나로 필드 주입과 비슷하게 작성이 가능하니 사용도 수정자 기반 주입보다 편리한 것 같습니다.

반응형
Comments