Dependency Injection

Dev.Hammy·2024년 2월 2일
0

DI(종속성 주입)는 객체가 자신의 종속성(즉, 작업하는 다른 객체)을 정의하는 프로세스입니다. 오로지 생성자 인수, 팩토리 메서드에 대한 인수 또는 생성된 후 객체 인스턴스에 설정된 속성, 팩토리 메서드에서 반환된 값을 통해서만 객체의 종속성을 정의하는 프로세스입니다.

그런 다음 컨테이너는 Bean을 생성할 때 해당 종속성을 주입합니다. 이 프로세스는 근본적으로 클래스 또는 서비스 로케이터 패턴의 생성자를 직접 사용하여 Bean이 가진 종속성의 인스턴스화 또는 위치를 제어하는 Bean 자체의 역(따라서 이름, 제어 역전)입니다.

DI 원칙을 사용하면 코드가 더 깔끔해지며, 객체에 종속성이 제공될 때 분리(decoupling)가 더 효과적입니다. 개체는 종속성을 조회하지 않으며 종속성의 위치나 클래스를 알지 못합니다.

결과적으로, 특히 단위 테스트에서 스텁 또는 모의 구현을 허용하는 추상 베이스 클래스나 인터페이스 위에 종속성이 있는 경우, 클래스를 테스트하기가 더 쉬워집니다.

DI는 생성자 기반 종속성 주입과 Setter 기반 종속성 주입이라는 두 가지 주요 변형으로 존재합니다.

Constructor-based Dependency Injection

생성자 기반 DI는 각각 종속성을 나타내는 여러 인수를 사용하여 생성자를 호출하는 컨테이너에 의해 수행됩니다. Bean을 구성하기 위해 특정 인수를 사용하여 static 팩토리 메소드를 호출하는 것은 거의 동등하며, 이 토론에서는 생성자에 제공되는 인수와 static 팩토리 메소드에 대한 인수를 유사하게 처리합니다. 다음 예제에서는 생성자 주입을 통해서만 종속성 주입이 가능한 클래스를 보여줍니다.

public class SimpleMovieLister {

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

	// a constructor so that the Spring container can inject a MovieFinder
	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

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

이 클래스에는 특별한 것이 없습니다. 이는 컨테이너별 인터페이스, 베이스 클래스 또는 어노테이션에 대한 종속성이 없는 POJO입니다.

Constructor Argument Resolution

생성자 인수 resolution matching는 인수 type을 사용하여 발생합니다. Bean 정의의 생성자 인수에 잠재적인 모호성(ambiguity)이 없으면 Bean 정의에서 생성자 인수가 정의되는 순서는 Bean이 인스턴스화될 때 해당 인수가 적절한 생성자에 제공되는 순서입니다. 다음 클래스를 고려해보세요:

package x.y;

public class ThingOne {

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

ThingTwoThingThree 클래스가 상속으로 관련되어 있지 않다고 가정하면, 잠재적인 모호성은 존재하지 않습니다. 따라서 다음 구성은 제대로 작동하며 <constructor-arg/> 요소에 생성자 인수 인덱스 또는 유형을 명시적으로 지정할 필요가 없습니다.

<beans>
	<bean id="beanOne" class="x.y.ThingOne">
		<constructor-arg ref="beanTwo"/>
		<constructor-arg ref="beanThree"/>
	</bean>

	<bean id="beanTwo" class="x.y.ThingTwo"/>

	<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

다른 Bean이 참조되면 type이 알려지고 matching이 발생할 수 있습니다(이전 예제의 경우와 마찬가지로). <value>true</value>와 같은 단순 유형이 사용되면 Spring은 값의 유형을 결정할 수 없으므로 도움 없이 type을 match시킬 수 없습니다. 다음 클래스를 고려해보세요:

package examples;

public class ExampleBean {

	// Number of years to calculate the Ultimate Answer
	private final int years;

	// The Answer to Life, the Universe, and Everything
	private final String ultimateAnswer;

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

이전 시나리오에서 다음 예제와 같이 type 속성(attribute)을 사용하여 생성자 인수의 type을 명시적으로 지정하는 경우 컨테이너는 단순 type과 matching하는 형식을 사용할 수 있습니다.

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg type="int" value="7500000"/>
	<constructor-arg type="java.lang.String" value="42"/>
</bean>

다음 예제와 같이 index 속성(attribute)을 사용하여 생성자 인수의 인덱스를 명시적으로 지정할 수 있습니다.

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg index="0" value="7500000"/>
	<constructor-arg index="1" value="42"/>
</bean>

여러 단순 값의 모호성을 해결하는 것 외에도 인덱스를 지정하면 생성자가 동일한 type의 인수를 두 개 이상 가질 때의 모호성을 해결합니다.

[Note] 인덱스는 0부터 시작합니다.

생성자 인수 이름

다음 예제와 같이 값 명확성(disambiguation)을 위해 생성자 매개 변수 이름을 사용할 수도 있습니다.

<bean id="exampleBean" class="examples.ExampleBean">
	<constructor-arg name="years" value="7500000"/>
	<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

이 작업을 즉시 수행하려면 Spring이 생성자에서 매개변수 이름을 찾을 수 있도록 디버그 플래그를 활성화하여 코드를 컴파일해야 합니다. 디버그 플래그를 사용하여 코드를 컴파일할 수 없거나 컴파일하고 싶지 않은 경우 @ConstructorProperties JDK annotation을 사용하여 생성자 인수의 이름을 명시적으로 지정할 수 있습니다. 그러면 샘플 클래스는 다음과 같아야 합니다.

package examples;

public class ExampleBean {

	// Fields omitted

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

Setter-based Dependency Injection

Setter 기반 DI는 빈을 인스턴스화하기 위해 인수 없는 생성자 또는 인수 없는 static 팩토리 메소드를 호출한 후 빈에서 setter 메소드를 호출하는 컨테이너에 의해 수행됩니다.

다음 예제에서는 순수 setter 주입을 통해서만 종속성 주입이 가능한 클래스를 보여줍니다. 이 클래스는 일반적인 Java입니다. 이는 컨테이너별 인터페이스, 베이스 클래스 또는 어노테이션에 대한 종속성이 없는 POJO입니다.

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는 관리하는 Bean에 대해 생성자 기반 및 setter 기반 DI를 지원합니다. 또한 생성자 접근 방식을 통해 일부 종속성이 이미 주입된 후의 경우에도 setter 기반 DI를 지원합니다. 속성(property)을 한 형식에서 다른 형식으로 변환하기 위해 PropertyEditor 인스턴스와 함께 사용하는(use in conjunction with) BeanDefinition 형식으로 종속성을 구성합니다. 그러나 대부분의 Spring 사용자는 이러한 클래스를 직접(즉, 프로그래밍 방식으로) 사용하지 않고 XML bean 정의, annotated 구성 요소(component)(즉, @Component, @Controller 등으로 어노테이션이 달린 클래스) 또는 Java 기반 @Configuration 클래스에서 @Bean 메서드를 사용하여 작업합니다. 그런 다음 이러한 소스는 내부적으로 BeanDefinition의 인스턴스로 변환되고 전체 Spring IoC 컨테이너 인스턴스를 로드하는 데 사용됩니다.

생성자(constructor) 기반 또는 setter 기반 DI?

생성자 기반 DI와 setter 기반 DI를 혼합할 수 있으므로 필수 종속성에는 생성자를 사용하고 선택적 종속성에는 setter 메서드 또는 구성(configuration) 메서드를 사용하는 것이 좋습니다. setter 메소드에 @Autowired 어노테이션을 사용하면 해당 속성을 필수 종속성(required dependency)으로 만들 수 있습니다. 그러나 프로그래밍 방식으로 인수를 검증하는 생성자 주입이 더 좋습니다.

Spring 팀은 일반적으로 생성자 주입을 주창(advocate)합니다. 이를 통해 애플리케이션 구성 요소를 불변 객체(immutable objects)로 구현하고 필요한 종속성이 null이 아니란 것을 확인할 수 있기 때문입니다. 또한 생성자 주입 컴포넌트(component)는 항상 완전히 초기화된 상태로 클라이언트(호출, calling) 코드에 반환됩니다. 참고로, 생성자 인수가 너무 많으면 클래스에 책임이 너무 많을 가능성이 높으며, 문제를 적절하게 분리하기 위해 리팩터링해야 함을 의미합니다.

Setter 주입은 주로 클래스 내에서 합리적인 기본값을 할당할 수 있는 선택적 종속성에만 사용해야 합니다. 그렇지 않으면 코드가 종속성을 사용하는 모든 곳에서 not-null 검사를 수행해야 합니다. setter 주입의 한 가지 이점은 setter 메서드가 해당 클래스의 객체를 나중에 재구성(reconfiguration)하거나 다시 주입(reinjection)할 수 있도록 한다는 것입니다. 따라서 JMX MBean을 통한 관리는 setter 주입에 대한 설득력 있는 사용 사례입니다.

특정 클래스에 가장 적합한 DI 스타일을 사용하십시오. 때로는 소스가 없는 타사 클래스를 처리할 때 선택이 자동으로 이루어집니다. 예를 들어, 타사 클래스가 setter 메서드를 노출하지 않는 경우 생성자 주입은 유일하게 사용 가능한 DI 형태일 수 있습니다.

Dependency Resolution Process


컨테이너는 다음과 같이 Bean 종속성 해결(resolution)을 수행합니다.

  • ApplicationContext는 모든 Bean을 설명하는 구성 메타데이터로 생성되고 초기화됩니다. 구성 메타데이터는 XML, Java 코드 또는 어노테이션으로 지정할 수 있습니다.

  • 각 Bean에 대해 해당 종속성은 속성(properties), 생성자 인수 또는 정적 팩토리 메서드에 대한 인수(일반 생성자 대신 이를 사용하는 경우) 형식으로 표현됩니다. 이러한 종속성은 Bean이 실제로 생성될 때 Bean에 제공됩니다.

  • 각 속성(property)이나 생성자 인수는 설정(set)할 값의 실제 정의이거나 컨테이너의 다른 Bean에 대한 참조(reference)입니다.

  • 값인 각 속성 또는 생성자 인수는 지정된 형식에서 해당 속성 또는 생성자 인수의 실제 형식으로 변환됩니다. 기본적으로 Spring은 String 형식으로 제공된 값을 int, long, String, boolean 등과 같은 모든 내장 유형(built-in type)으로 변환할 수 있습니다.

Spring 컨테이너는 컨테이너가 생성될 때 각 Bean의 구성(configuration)을 검증합니다. 그러나 Bean 속성(property) 자체는 Bean이 실제로 생성될 때까지 설정되지 않습니다. 싱글톤 범위이고 사전-인스턴스화(pre-instantiated)(기본값 the default)되도록 설정된 Bean은 컨테이너가 생성될 때 생성됩니다. 범위는 Bean 범위에 정의됩니다. 그렇지 않으면 요청될 때만 Bean이 생성됩니다. Bean을 생성하면 Bean의 종속성과 해당 종속성의 종속성(등)이 생성되고 할당되므로 잠재적으로 Bean의 그래프가 생성됩니다. 이러한 종속성 간의 resolution mismatch는 늦게, 즉 영향을 받은 Bean을 처음 생성할 때 나타날 수 있습니다.

순환 종속성

주로(predominantly) 생성자 주입을 사용하는 경우 해결할 수 없는 순환 종속성 시나리오를 생성할 수 있습니다.

예를 들어 클래스 A에는 생성자 주입을 통해 클래스 B의 인스턴스가 필요하고, 클래스 B에는 생성자 주입을 통해 클래스 A의 인스턴스가 필요합니다. 클래스 A와 B가 서로 주입되도록 빈을 구성하면 Spring IoC 컨테이너는 런타임에 이 순환 참조를 감지하고 BeanCurrentlyInCreationException을 발생시킵니다.

한 가지 가능한 해결책은 생성자가 아닌 setter로 구성(configure)되도록 일부 클래스의 소스 코드를 편집하는 것입니다. 또는 생성자 주입을 피하고 setter 주입만 사용하십시오. 즉, 권장되지는 않지만 setter 주입을 통해 순환 종속성을 구성할 수 있습니다.

순환 종속성이 없는 일반적인 경우와 달리 Bean A와 Bean B 사이의 순환 종속성은 완전히 초기화되기 전에 Bean 중 하나가 다른 Bean에 주입되도록 강제합니다(전통적인 닭고기와 달걀 시나리오).

일반적으로 Spring이 올바른 일을 수행한다고 신뢰할 수 있습니다. 컨테이너 로드 시 존재하지 않는 Bean에 대한 참조 및 순환 종속성과 같은 구성(configuration) 문제를 감지합니다. Spring은 가능한 한 늦게(=빈이 실제로 생성될 때) 속성(property)을 설정(set)하고 종속성(dependency)을 해결(resolve)합니다. 이는 올바르게 로드된 Spring 컨테이너가 나중에 당신이 객체를 요청했을때, 그 객체 또는 그것의 종속성을 생성할 때 문제가 있는 경우 예외를 발생시킬 수도 있다는 뜻입니다. 예를 들어 빈은 누락되거나 유효하지 않은 속성(property)에 대한 결과로 예외를 발생시킵니다.



일부 구성 문제에 대해, 잠재적으로 지연된 가시성(visibility)이 ApplicationContext 구현이 기본적으로 싱글톤 bean을 사전 인스턴스화하는 이유입니다. 실제로 필요하기 전에 이러한 Bean을 생성하는 데 약간의 선행 시간과 메모리를 희생하면, (나중이 아니라) ApplicationContext가 생성될 때 구성(configuration) 문제를 발견하게 됩니다. 싱글톤 bean이 즉시(eagerly) 사전 인스턴스화되는 대신 느리게(lazily) 초기화되도록 이 기본 동작을 재정의(override)할 수 있습니다.

순환 종속성이 존재하지 않는 경우 하나 이상의 협력(collaborating) bean이 종속 bean에 주입되면 각 협력 Bean은 종속 Bean에 주입되기 전에 온전히 구성(totally configured)됩니다. 이는 Bean A가 Bean B에 종속성을 갖는 경우 Spring IoC 컨테이너가 Bean A에서 setter 메소드를 호출하기 전에 Bean B를 완전히 구성한다는 것을 의미합니다. 즉, Bean은 인스턴스화됩니다(사전 인스턴스화된 싱글톤이 아닌 경우). 그것의 해당 종속성이 설정(set)되고 관련 lifecycle 메서드(예: 구성된 init 메서드 또는 InitializingBean 콜백 메서드)가 호출됩니다.

Examples of Dependency Injection

다음 예에서는 setter 기반 DI에 XML 기반 구성 메타데이터를 사용합니다. Spring XML 구성 파일의 작은 부분은 다음과 같이 일부 Bean 정의를 지정합니다

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- setter injection using the nested ref element -->
	<property name="beanOne">
		<ref bean="anotherExampleBean"/>
	</property>

	<!-- setter injection using the neater ref attribute -->
	<property name="beanTwo" ref="yetAnotherBean"/>
	<property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

다음 예제에서는 해당하는 ExampleBean 클래스를 보여줍니다.

public class ExampleBean {

	private AnotherBean beanOne;

	private YetAnotherBean beanTwo;

	private int i;

	public void setBeanOne(AnotherBean beanOne) {
		this.beanOne = beanOne;
	}

	public void setBeanTwo(YetAnotherBean beanTwo) {
		this.beanTwo = beanTwo;
	}

	public void setIntegerProperty(int i) {
		this.i = i;
	}
}

앞의 예에서 setter는 XML 파일에 지정된 속성(property)과 일치하도록 선언되었습니다. 다음 예에서는 생성자 기반 DI를 사용합니다.

<bean id="exampleBean" class="examples.ExampleBean">
	<!-- constructor injection using the nested ref element -->
	<constructor-arg>
		<ref bean="anotherExampleBean"/>
	</constructor-arg>

	<!-- constructor injection using the neater ref attribute -->
	<constructor-arg ref="yetAnotherBean"/>

	<constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

다음 예제에서는 해당하는 ExampleBean 클래스를 보여줍니다.

public class ExampleBean {

	private AnotherBean beanOne;

	private YetAnotherBean beanTwo;

	private int i;

	public ExampleBean(
		AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
		this.beanOne = anotherBean;
		this.beanTwo = yetAnotherBean;
		this.i = i;
	}
}

Bean 정의에 지정된 생성자 인수는 ExampleBean 생성자에 대한 인수로 사용됩니다.

객체의 인스턴스를 반환하기 위해, 이제 Spring에게 생성자를 사용하는 대신 static 팩토리 메서드를 호출하도록 지시하는 이 예제의 변형을 고려해 보세요.

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
	<constructor-arg ref="anotherExampleBean"/>
	<constructor-arg ref="yetAnotherBean"/>
	<constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

다음 예제에서는 해당하는 ExampleBean 클래스를 보여줍니다.

public class ExampleBean {

	// a private constructor
	private ExampleBean(...) {
		...
	}

	// a static factory method; the arguments to this method can be
	// considered the dependencies of the bean that is returned,
	// regardless of how those arguments are actually used.
	public static ExampleBean createInstance (
		AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

		ExampleBean eb = new ExampleBean (...);
		// some other operations...
		return eb;
	}
}

static 팩토리 메소드에 대한 인수는 <constructor-arg/> 요소에 의해 제공(supplied)되며, 이는 생성자가 실제로 사용된 것과 정확히 동일합니다. 팩토리 메서드에서 반환되는 클래스의 타입은 static 팩터리 메서드를 포함하는 클래스와 동일한 타입일 필요는 없습니다(단, 이 예에서는 동일함). 인스턴스(non-static) 팩토리 메소드는 본질적으로 동일한 방식(fashion)으로 사용될 수 있으므로(class 속성(attribute) 대신 factory-bean 속성(attribute)을 사용하는 것을 제외하고(aside from)) 여기서는 자세한 내용을 논의하지 않습니다.

0개의 댓글