Spring bean (2) 의존성 주입

굴착드릴·2024년 8월 14일

의존성 주입

Bean의 생성, 초기화는 Spring framework가 해줍니다.

이 때 생성, 초기화 과정에서 필요한 의존성을 Spring framework가 찾아 주입해줍니다.

생성 VS 초기화

생성은 객체가 new 연산자를 통해 생성된 것이며 초기화는 해당 객체에 필요한 의존성을 갖추는 것을 의미합니다.

class Example {
private String a;

public void setA(String given) {
	this.a = given;
	}
}

Example example = new Example(); // 생성
example.setA("hello"); // 초기화

Bean에 의존성이 주입되는 주요한 방법은 3가지입니다.

  • 필드 주입
  • Setter 주입
  • 생성자 주입

필드 주입

@Autowired 어노테이션을 사용하여 클래스의 필드에 직접 의존성을 주입합니다.

public class Service {
    @Autowired
    private Dependency dependency;
}

장점

  • 코드가 간결해집니다.
    • 의존성을 주입하기 위해 별도의 setter나 생성자를 작성할 필요가 없습니다.

단점

  • 테스트하기가 어렵습니다.
    • 필드를 직접 주입받기 때문에, 객체 생성 후에 의존성을 주입할 방법이 제한적입니다.
  • Spring 컨테이너가 필요하여, DI 프레임워크에 의존적이 됩니다.
  • 객체가 불변 상태를 유지할 수 없습니다.
    • 객체 생성 시점과 초기화 시점이 다르기 때문에 의존성에 final 키워드 사용이 불가능합니다.
  • 순환참조를 로직 호출 시에서 알 수 있습니다.
    • 의존하고 있는 객체가 생성되지 않아도 의존받는 객체가 생성될 수 있어 Spring 초기화 단계에서 순환참조를 잡아낼 수 없습니다.
    • Spring boot 2.6^에서는 Spring context 초기화 단계에서 순환참조를 발견할 수 있습니다.

🚨 간결하고 직관적이지만 위 단점들로 가장 피해야 할 주입방식입니다.

IntelliJ에서는 필드 주입을 사용하면 "Field Injection is not recommended"라는 경고문을 띄우고, 스프링 공식 레퍼런스에는 소개조차 되지 않습니다.

Setter 주입

의존성이 필요한 필드에 대해 setter 메서드를 정의하고 Spring 컨테이너가 이 메서드를 호출하여 의존성을 주입합니다.

public class Service {
    private Dependency dependency;

    public void setDependency(Dependency dependency) {
        this.dependency = dependency;
    }
}

장점

  • 선택적인 의존성 주입이 가능합니다. 즉, 특정 의존성은 필수적이지 않을 때 사용할 수 있습니다.
  • 객체 생성 후에 의존성을 주입할 수 있으므로, 객체가 생성된 후에 동적으로 의존성을 설정하는 데 유용합니다.

단점

  • 주입되지 않은 경우, NPE(NullPointerException)과 같은 예외가 발생할 수 있으므로 안전성에 문제가 있을 수 있습니다.
  • 객체의 상태가 변경 가능(mutable)하여, 불변 객체를 만들기 어렵습니다.
    • final 키워드 역시 사용 불가능합니다.

생성자 주입

의존성을 필요한 필드를 생성자의 매개변수로 받아들이고, 이를 통해 주입을 받습니다. Spring 4.3부터는 생성자가 하나만 있을 경우 @Autowired 어노테이션을 생략할 수 있습니다.

public class Service {
    private final Dependency dependency;

    @Autowired // 생략 가능
    public Service(Dependency dependency) {
        this.dependency = dependency;
    }
}
@Service
public class Service {

    private final Dependency1 dependency1;
    private final Dependency2 dependency2;

    public Service(Dependency1 dependency1) {
        this.dependency1 = dependency1;
        this.dependency2 = null;
    }

    @Autowired // 생략 불가
    public Service(Dependency1 dependency1, Dependency2 dependency2) {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }
}

생성자가 두 개 이상인 경우 반드시 1개의 생성자에 @Autowired를 명시해주어야 합니다.

Spring은 @Autowired가 붙은 생성자를 통해 의존성을 주입합니다. 따라서 두 생성자에 모두 @Autowired를 붙이면 Spring이 어느 생성자를 사용할지 혼란스러워 할 수 있으며, 이는 예외를 발생시킬 수 있습니다.

장점

  • 의존성이 주입되지 않으면 객체를 생성할 수 없으므로, 의존성 누락으로 인한 오류를 방지할 수 있습니다.
  • 객체가 불변(immutable) 상태를 유지할 수 있어, 객체의 안정성이 높아집니다.
  • 단위 테스트 작성이 용이하며, DI 프레임워크에 종속적이지 않습니다.
  • 스프링 context 초기화 단계에서 순환참조를 발견할 수 있습니다.

단점

  • 의존성이 많은 경우 생성자 파라미터가 많아져 코드 가독성이 떨어질 수 있습니다.
    • 해당 경우 lombok의 @RequiredArgsConstructor를 통해 해결할 수 있습니다.

가장 선호되는 방식입니다.

불변객체

불변객체란

불변 객체(Immutable Object)는 객체가 생성된 후 그 상태를 변경할 수 없는 객체를 의미합니다.

Spring에서 bean이 상태를 가지게 되면 발생하는 문제

Spring은 multi-thread 환경에서 작동되며 각 Bean들은 Singletone으로 관리되어 재사용됩니다.

Singletone으로 관리되는 객체의 경우 상태를 가지게 되면 이전 호출에서 가졌던 상태가 이후 호출에서 영향을 미치게 됩니다.

이런 상황에서 muli-thread 환경까지 겹치게 되면 개발자는 상태 추적(예상, 관리)가 불가능할 수 있습니다.

As a rule, you should use the prototype scope for all stateful beans and the singleton scope for stateless beans.
from Spring docs

profile
두두두두..

0개의 댓글