본문 바로가기
Spring

Dependency Injection - 스프링 공식문서가 말하는 의존성 주입

by irerin07 2024. 1. 23.
728x90
  • 의존성 주입(Dependency Injection, DI)은 펙토리 메서드를 통해 객체가 생성되거나 반환된 후 객체 인스턴스에 설정된 속성으로, 또는 생성자 인수나 펙토리 메서드에 대한 인수를 통해서만 객체가 가진 의존성을 정의하는 프로세스이다.
  • 컨테이너는 정의된 의존성을 바탕으로 빈이 생성될 때 필요한 의존성을 주입해준다.
  • 의존성 주입은 빈이 자신에게 필요한 의존성을 직접 제어하는 방식을 "역전" 시킨 것으로, 이를 IoC(Inversion of Control, 제어의 역전)이라 한다.
  • 의존성 주입을 사용하면 더 깔끔한 코드를 짤 수 있고, 객체한 결합을 분리하기가 더 용이해진다.
    • 객체는 자신에게 필요한 의존성(다른 객체들)이 어디에 있는지 알 필요도 없고, 직접 찾을 필요도 없어진다.
    • 그 결과로 테스트가 더 용이해지게 된다.
  • 의존성 주입은 크게 두가지 방법이 있다.
    • 생성자 주입
    • Setter 주입

Constructor-based Dependency Injection - 생성자 주입 방식의 DI

  • 생성자 주입 방식은 컨테이너가 의존성을 나타내는 인수를 가진 생성자를 호출하면서 동작한다.
  • static 팩토리 메서드를 사용한 방식도 이와 비슷하게 실행된다.
public class SimpleMovieLister { 
	// SimpleMovieLister는 MovieFinder에 의존성을 가진다. 
    private final MovieFinder movieFinder; 
    // 생성자를통해 스프링 컨테이너가 필요한 의존성, 여기서는 MovieFinder를 주입할 수 있도록 한다. 
    public SimpleMovieLister(MovieFinder movieFinder) { 
    	this.movieFinder = movieFinder; 
    } 
    
    // 주입된 의존성을 사용하는 비즈니스 로직들 
    
}
  • 해당 클래스는 별로 특별한 것이 없습니다. 특정 인터페이스, 기본 클래스 또는 애노테이션에 의존성이 없는 단순한 POJO입니다.

Constructor Argument Resolution - 생성자 인수 결정

  • 생성자 인수 결정 매칭은 인수의 타입으로 일어납니다.
  • 빈 정의의 생성자 인수에 잠재적인 모호성이 존재하지 않는 경우, 빈 정의(Bean Definition)에서 생성자 인수의 순서는 빈이 인스턴스화 할때 제공되는 인수들의 순서로 정의된다.
package x.y; 

public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
    //....
}

}
  • 위 코드에서 보이는 ThingTwo와 ThingThree 클래스는 상속관계도 아니고 다른 어떤 모호성도 존재하지 않습니다.
  • 그렇기에 다음 설정은 아무 문제없이 잘 작동하며, element에 추가적으로 생성자 인수의 인덱스나 타입을 별도로 지정할 필요가 없습니다.
<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
  • 하지만 다음과 같은 경우 스프링은 추가적인 도움 없이는 매칭을 할 수 없습니다.
package examples;

public class ExampleBean {

private final int years;

private final String ultimateAnswer;

public ExampleBean(int years, String ultimateAnswer) {
    this.years = years;
    this.ultimateAnswer = ultimateAnswer;
}

}
  • 위 예시에서 컨테이너는 다음과 같이 type 애트리뷰트를 사용해 제공된 생성자 인수의 타입을 사용해 매칭을 사용하게 됩니다.
  • 혹은 index 애트리뷰트를 사용하여 매칭을 사용할 수도 있습니다.
  • 혹은 생성자 인수의 이름을 사용할 수도 있습니다.
  • 다만 생성자 인수의 이름을 사용하는 방식을 사용하기 위해서는 디버그 플래그를 enable로 설정해야 합니다. 그래야지만 스프링이 생성자에서 파라미터 이름을 찾을 수 있습니다. - 혹은 디버그 플래그대신 @ConstructorProperties를 사용할 수도 있습니다.
package examples;

public class ExampleBean {

// Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})  
    public ExampleBean(int years, String ultimateAnswer) {  
        this.years = years;  
        this.ultimateAnswer = ultimateAnswer;  
    }

}

Setter Argument Resolution - 수정자 인수 결정

  • 수정자 주입 방식은 no-argument 생성자 혹은 no-argument static 팩토리 메서드를 호출한 뒤 컨테이너가 빈의 setter 메서드를 호출하는 방식으로 동작합니다.
public class SimpleMovieLister {

	// the SimpleMovieLister has a dependency on the MovieFinder
	private MovieFinder movieFinder;

	// a setter method so that the Spring container can inject a MovieFinder
	public void setMovieFinder(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// business logic that actually uses the injected MovieFinder is omitted...
}

 

  • ApplicationContext는 자신이 관리하는 빈에 대해 생성자 주입 방식과 수정자 주입 방식 모두를 지원합니다.
    • 거기에 더해 이미 생성자 주입으로 의존성이 주입된 이후 해당 빈에 수정자 주입 방식으로 의존성을 주입하는 것도 지원합니다.
  • 이러한 의존성은 BeanDefinition형식으로 되어있으며 PropertyEditor 인스턴스와 함께 활용해 한 형식에서 다른 형식으로 변환하여 설정합니다.
  • 하지만 대부분의 스프링 사용자들은 이런 방식보다는 XML을 활용하거나 애노테이션을 사용합니다.
    • 이런 방식들도 스프링 내부에서 BeanDefinition으로 변환되고 스프링 IoC 컨테이너 인스턴스를 불러오는데 사용됩니다.
생성자 주입 방식 vs 수정자 주입 방식

생성자 주입 방식과 수정자 주입 방식을 혼용할 수 있기 때문에, 가장 보편적인 방법은 필수 의존성 주입은 생성자 주입 방식을 사용하고 그 외에는 수정자 주입 방식 혹은 설정(XML, 자바 애노테이션, 자바 코드 방식등)을 사용하여 의존성 주입을 하는 것입니다.

스프링팀은 생성자 주입 방식을 좀 더 선호하는데, 이는 애플리케이션 컴토넌트를 불변으로 구현할 수 있게 해주며 필수 의존성들이 Null이 아님을 보장해주기 때문입니다. 추가적으로, 생성자 주입 방식으로 생성된 컴포넌트는 항상 완전히 초기화된 상태로 클라이언트 코드로 반환됩니다. 

여담으로, 너무 많은 생성자 인수는 Code Smell을 나타냅니다. 이는 보통 해당 클래스가 너무 많은 책임을 가지고 있음을 알려주고 리팩터링을 통해 이를 분리해야 함을 알려줍니다.

수정자 주입 방식을 사용하고자 하는 경우 필수가 아닌 의존성을 주입하는 경우에만 사용하는 것이 좋지만, 이를 필수 의존성을 주입하고자 하는 경우엔 해당 의존성을 사용하는 모든 곳에서 널체크를 진행해야 합니다.

수정자 주입의 장점 중 하나는 해당 클래스의 객체를 나중에 재구성 하거나 재주입할 수 있도록 만들어 준다는 점에 있습니다. 따라서 JMX MBeans를 활용한 빈의 관리는 수정자 주입에 대한 강력한 사용 사례입니다.

각 클래스마다 사용할 수 있는 DI 방식이 다를 수 있습니다. 예를들어 Setter 메소드를 노출하지 않는 클래스에서는 생성자 주입만이 사용 가능한 방식일 수 있습니다.

 

Dependency Resolution Process - 의존성 결정 과정

  • 컨테이너는 다음과 같이 빈 의존성 결정을 진행합니다.
    1. XML, 자바 코드 또는 자바 애노테이션을 활용한 Configuration Metadata에 등록된 빈 정보를 바탕으로 ApplicationContext가 생성되고 초기화 됩니다.
    2. 각각의 빈 마다 필요한 의존성이 여러 형태로 표현됩니다. properties, 생성자 인수, static 팩토리 메서드 인수가 대표적입니다. 이런 의존성들은 빈이 실제로 생성될 때 제공됩니다.
    3. 전달된 properties나 생성자 인수는 빈에 set할 실제 값이거나 컨테이너에 있는 다른 빈의 참조값입니다.
    4. 전달된 properties나 생성자 인수 값들은 자신이 지정된 형태에서 properties나 생성자 인수에서 요하는 형식으로 변경될 수 있습니다. 기본적으로 스프링은 string 포맷으로 전달된 값을 int, long, String, boolean 그리고 이 외의 다른 형식으로 변환할 수 있습니다.
  • 스프링 컨테이너가 생성되면서 각 빈들의 설정을 검증합니다. 하지만 이런 설정 정보들은 실제 빈이 생성되지 전에는 빈에 설정되지 않습니다.
  • 빈의 Scope 설정이 Singleton이거나 pre-instantiated로 설정된 빈들은 컨테이너가 생성되는 동시에 함께 생성됩니다.
    • 그외의 빈들은 해당 빈에 대한 요청이 들어왔을 때 생성됩니다.
  • 빈을 생성하면서 생성 빈에 대한 그래프가 만들어 질 수 있는데, 이는 빈의 의존성, 그 의존성에 대한 의존성 이런식으로 관계가 생성되고 배분되기 때문입니다.
Circular dependencies - 순환 참조

주로 생성자 주입 방식을 사용한다면 해결하기 불가능한 순환참조를 만들 수 있습니다.

예를 들어 A클래스는 B클래스의 인스턴스를 생성자 주입 방식으로 요청하고, B 클래스는 A클래스의 인스턴스를 생성자 주입 방식으로 요청하는 상황을 생각해 봅시다.
이렇게 A클래스의 빈과 B클래스의 빈이 서로에게 주입되도록 설정한다면, 스프링 IoC 컨테이너는 이런 순환 참조를 런타임시에 감지하여 BeanCurrentlyInCreationException을 발생시킵니다.

이를 해결하기 위한 방법중 하나는 바로 순환 참조가 발생한 클래스 중 하나를 생성자 주입 대신 수정자 주입을 사용하는 것이다. 아니면 생성자 주입을 사용하지 않고 수정자 주입만 사용하는 것도 하나의 방법이다.
  • 존재하지 않는 빈을 참조하거나 순환 참조 같은 문제가 산재해 있다 하더라도 스프링은 컨테이너 로드 타임에 이런 설정 관련 문제들을 감지할 수 있다.
  • 스프링은 설정 정보를 set 하고 의존성 처리 하는 작업을 최대한 미루는데, 실제 빈이 생성되는 순간까지 미룹니다.
    • 이는 정상적으로 load 된 Container가 어떤 객체가 요청 되었을 때 해당 객체를 생성하거나, 객체의 의존성을 만드는 과정에서 오류가 발생할 수도 있다는 뜻입니다.
  • 이렇듯 몇몇 설정들의 가시성이 지연되는 이슈 때문에 ApplicationContext 구현체들은 기본적으로 싱글턴 빈들을 pre-instantiate 방식으로 생성합니다.
    • 실제 빈이 필요한 시점이 아닌 더 이른 시점에 생성( pre-instantiate) 하기 위한 초기 실행 시간과 메모리를 대가로 ApplicationContext가 생성될 때 순환 참조등의 설정 정보 오류들을 잡아낼 수 있습니다.
    • 하지만 언제든지 빈의 생성 시점은 override하여 변경할 수 있습니다.
  • 아무런 문제가 없다면 의존성 주입에 필요한 빈들이 의존성 주입이 필요한 빈 이전에 먼저 설정됩니다.
  • 예를 들어 A빈이 B빈에 의종한다면, 스프링 IoC 컨테이너는 A빈의 setter 메서드를 호출하기 전, B빈을 먼저 설정합니다.
  • 다시 말해, 빈이 인스턴스화 되고(만약 pre-instantiated 싱글턴이 아니라면), 의존성이 부여되며, 연관있는 lifecycle 메서드들(configured init method, initializingBean callback method등)이 호출됩니다.

 

출처 : https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html

728x90