-
[Spring Core] 공식문서 공부 - 1.4.1 Dependency InjectionJAVA/Spring 2022. 8. 25. 17:45
Core Technologies
In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do
docs.spring.io
이번 글에서는 Spring IoC 컨테이너를 통해서 의존성 주입(Dependency Injection)을 하는 여러가지 방법에 대해 다룬다.
먼저 공식문서 초반에 의존성 주입(Dependency Injection)을 정의했던 것을 떠올려보자.
의존성 주입(Dependency Injection)
객체가 그들의 의존성(함께 사용되는 다른 객체들)을 아래의 3가지 방식을 통해서만 정의할 수 있다는 것.
1. 생성자 인수(Constructor Arguments)
2. 팩토리 메소드로의 인수(Arguments to Factory Method)
3. 팩토리 메소드로부터 리턴되거나 생성된 객체 인스턴스 내에 있는 프로퍼티들Spring IoC 컨테이너는 새로운 Bean을 생성할 때 위의 정의대로 의존성을 주입한다.
다음으로, 제어의 역전(IoC, Inversion of Control)에 대해서도 얘기했었다.
제어의 역전(IoC, Inversion of Control)
Bean은 자신의 인스턴스화와 자신의 의존성들의 위치를 컨트롤하기 위해
직접 클래스를 생성하거나 Service Locator 패턴과 같은 매커니즘을 사용하는데,
이 프로세스 자체가 근본적으로 역(제어의 역전)이기 때문이다.의존성 주입(DI)을 통해서 코드를 더 깔끔하게 만들거나, 더 효과적으로 의존성을 가진 객체들을 분리할 수 있다.
또한, 객체가 자신의 의존성들이나 의존성들의 위치 혹은 클래스를 알아야 할 필요가 없기 때문에 더 쉽게 코드를 테스트 할 수 있게된다.
이러한 의존성 주입의 방식에는 크게 2가지가 존재한다.
- 생성자 기반 의존성 주입
- Setter 기반 의존성 주입
Constructor-based Dependency Injection
생성자 기반 의존성 주입은 Spring IoC 컨테이너가 의존성들(dependencies)을 인수(arguments)로 받는 생성자를 호출함으로써 이루어진다. 아래 예시는 생성자 기반 의존성 주입으로만 의존성 주입을 받을 수 있는 클래스를 나타낸다.
public class SimpleMovieLister { // SimpleMovieLister 클래스는 MovieFinder 클래스에 의존성을 가지고 있음 private final MovieFinder movieFinder; // Spring IoC 컨테이너가 아래 생성자를 통해 MovieFinder 의존성을 주입할 수 있다. public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // 주입된 MovieFinder 의존성을 사용하는 코드는 생략... }이 SimpleMovieLister 클래스는 컨테이너에 특정된 인터페이스, 클래스, 어노테이션에 의존성을 가지지 않은 POJO(Plain old Java Object)이다.
Constructor Argument Resolution
생성자 인수 해석 매칭은 인수의 타입을 사용해서 진행된다. Bean 정의내 생성자 인수들간에 잠재적으로 모호한 점이 없다면, Bean 정의내 정의된 생성자 인수들의 순서와 Bean이 인스턴스화 될 때 해당 인수들이 생성자 인수로 전달되는 순서는 같다.
아래의 클래스를 살펴보자.
package x.y; public class ThingOne { public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { // ... } }여기서 ThingTwo 클래스와 ThingThree 클래스는 서로 상속관계가 아니며, 잠재적인 모호성이 없다고 가정하겠다.
그렇다면 아래 구성 메타데이터처럼 <constructor-arg /> 요소내에 인수들의 순서나 타입을 분명하게 명시해주지 않아도 Spring IoC 컨테이너가 별 문제없이 Bean들을 생성 할 것이다.
<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이 참조된다면 Spring IoC 컨테이너가 매칭할 수 있게된다.
하지만 <value>true<value/>와 같이 기본형이 사용됬을 경우 Spring IoC 컨테이너는 해당 값의 타입이 무엇인지 알 수 없기 때문에 별도의 도움 없이는 매칭을 할 수 없게된다.
Constructor argument type matching
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; } }위의 ExampleBean 클래스는 기본형 멤버변수 years, ultimateAnswer를 가지고 있다.
Spring IoC 컨테이너가 해당 클래스의 생성자 인수 매칭을 하기 위해서는 아래와 같이 구성 메타데이터에 생성자 인수의 타입을 명시해주어야 한다.
<bean id="exampleBean" class="examples.ExampleBean"> <!--"type" 속성에 생성자 인수의 타입을 명시함--> <constructor-arg type="int" value="7500000"/> <constructor-arg type="java.lang.String" value="42"/> </bean>Constructor argument index
아래처럼 "type" 속성 대신에 "index" 속성을 이용할수도 있다. "index" 속성에는 생성자 인수의 index를 명시하면 된다.
이는 생성자 인수들중에 같은 기본형인 인수가 2개 이상일 때 특히 유용하다.
<!--new ExampleBean(7500000, "42");--> <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg index="0" value="7500000"/> <constructor-arg index="1" value="42"/> </bean> <!--만약에 아래처럼 같은 기본형인 인수가 있으면 type 속성만으로는 매칭을 할 수 없다--> <bean id="exampleBean2" class="examples.ExampleBean2"> <constructor-arg type="int" value="1234"/> <constructor-arg type="int" value="5678"/> </bean>Index는 0부터 시작한다는 점을 주의하자.
Constructor argument name
아래처럼 생성자의 매개변수 이름을 사용할 수도 있다. (<constructor-arg /> 요소내의 name 속성 사용)
<!--public ExampleBean(int years, String ultimateAnswer) {...}--> <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg name="years" value="7500000"/> <constructor-arg name="ultimateAnswer" value="42"/> </bean>하지만 매개변수의 이름을 사용하는 방법은 debug flag가 허용된 상태로 컴파일 하거나 @ConstructorProperties 어노테이션을 사용해야 Spring IoC 컨테이너가 생성자로 부터 매개변수의 이름을 받아올 수 있다.
// @ConstructorProperties 어노테이션 사용 예제 package examples; public class ExampleBean { // 필드 생략 @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } }
Setter-based Dependency Injection
Setter 기반 의존성 주입은 해당 Bean을 인스턴스화 하기 위해 기본 생성자 또는 기본(no-arguments) 정적 스테틱 메소드를 호출 한 후에, Spring IoC 컨테이너가 해당 Bean 내의 setter 메소드를 호출함으로써 이루어진다.
아래는 setter 기반 의존성 주입을 통해서만 의존성을 주입 받을 수 있는 클래스 예시이다.
public class SimpleMovieLister { // SimpleMovieLister 클래스는 MovieFinder 클래스에 대한 의존성을 가지고 있다. private MovieFinder movieFinder; // Spring IoC 컨테이너는 아래의 setter 메소드를 통해 의존성을 주입한다. public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // 이하 생략... }ApplicationContext 인터페이스는 생성자 기반 의존성 주입과 setter 기반 의존성 주입을 모두 지원한다.
심지어 생성자 기반으로 의존성을 주입 받은 Bean에 대해 추가적으로 setter 기반으로 의존성들을 주입할 수 도 있다.
이렇게 하기 위해서는 BeanDefinition 인터페이스의 형태로 의존성들을 구성하면 된다. 하지만, 대부분의 사람들은 이러한 클래스들을 직접적으로 다루지 않고 XML 기반 구성 메타데이터내 Bean 정의를 이용한다. Spring IoC 컨테이너의 모든 인스턴스들 로드하기 위해서 이러한 Bean 정의들은 BeanDefinition 인터페이스의 인스턴스로 변환된다.
Constructor-based or Setter-based DI?
생성자 기반 의존성 주입과 setter 기반 의존성 주입 둘다 섞어서 사용해도 상관은 없다.
하지만 Spring 공식문서에서는 필수적인 의존성들에 대해서는 생성자 기반 의존성 주입을, 나머지들에 대해서는 settter 기반 의존성 주입을 사용하는 것을 추천하고 있다. Setter 메소드에 @Required 어노테이션을 사용하는 것은 프로퍼티들이 의존성을 필요로하도록 만든다고 한다. 하지만 생성자 기반 의존성 주입의 경우 프로그램적으로 인수들의 검증이 가능해서 선호된다고 한다.
Spring 팀은 생성자 기반 의존성 주입을 추천한다.
생성자 기반 의존성 주입을 사용하면 어플리케이션의 구성 요소들을 불변 객체로 만들 수 있고, 요구되는 의존성들이 null이 아니라는 것을 보장해주기 때문이다. 게다가 생성자 기반 의존성 주입을 통해 생성된 구성 요소들(components)은 항상 완전히 초기화된 상태로 이들을 호출한 코드(클라이언트)로 리턴된다.
Setter 기반 의존성 주입은 클래스내에서 적절한 기본값을 할당 받을 수 있는 선택적인 의존성들에 대해서만 사용해야한다.
그렇지 않으면 해당 의존성을 사용하는 모든 코드들에 대해 null이 아닌지 검증하는 절차를 수행해야 하기 때문이다. setter 기반 의존성 주입의 장점으로는 해당 클래스내 객체들이 나중에 재구성되거나 의존성을 다시 주입받을 수 있도록 만든다는 것이다.
개별 클래스들을 이해하기 쉽도록 만들어주는 의존성 주입(DI) 스타일을 사용하자.
Dependency Resolution Process
Spring IoC 컨테이너는 아래와 같이 Bean 의존성 해석(resolution)을 수행한다.
- ApplicationContext 인터페이스의 구현체는 모든 Bean들에 대해 묘사해놓은 구성 메타데이터를 통해서 생성되고 초기화된다.
- 각각의 Bean의 의존성들은 프로퍼티, 생성자 인수, 정적 팩토리 메소드로의 인수의 형태로 표현되어있다. 이 의존성들은 해당 Bean이 실제로 생성되는 시점에 해당 Bean으로 전달된다.
- 프로퍼티나, 생성자 인수의 경우는 값(value)에 대한 실제 정의나, 같은 IoC 컨테이너에 등록된 다른 Bean에 대한 참조이다.
- 프로퍼티나, 생성자 인수에 들어가는 값(value)들은 특정한 형식으로부터 실제 해당 변수의 타입으로 변환된다. 기본적으로 Spring은 String 형태로 들어온 값들을 빌트인 타입(int, long, String, boolean 등)으로 변환할 수 있다.
Spring IoC 컨테이너는 생성될때 각각의 Bean에 대한 구성(configuration)을 검증한다. 그러나 Bean의 프로퍼티들은 Bean이 실제로 생성되기 전까지는 세팅되지 않는다. 컨테이너가 생성될때 생성되는 Bean은 scope가 singleton이거나, pre-instantiated(기본값)로 설정된 경우만 해당된다. 나머지 경우는 해당 Bean을 요청받은 시점에 생성된다. 한 Bean의 생성은 잠재적으로 다른 Bean들의 생성을 야기한다. 해당 Bean의 의존성들과 이 의존성들에 대한 의존성들까지 생성되고 할당되어야 하기 때문이다. 해당 의존성들에 대한 의존성 해석(resolution)과정에서의 불일치는 이에 영향을 받는 Bean이 생성되는 시점(컨테이너 생성 시점보다 늦은 시점)에 뒤늦게 발견된다.
Circular Dependency
생성자 기반 의존성 주입을 사용하다보면 해결할 수 없는 순환 의존성(Circular Dependency)이 생기는 경우가 있다.
아래의 경우를 생각해보자.
클래스 A가 생성자 기반 의존성 주입을 받기 위해서는 클래스 B의 인스턴스가 필요하고,
반대로 클래스 B가 생성자 기반 의존성 주입을 받기 위해 클래스 A의 인스턴스가 필요한 상황이다.
여기서 클래스 A, B가 서로를 주입받도록 구성(configuration)하면, Spring IoC 컨테이너가 런타임에 순환 의존성을 발견하고 BeanCurrentlyCreationException 에러를 발생시킨다(throw).
이 문제를 해결 할 수 있는 방법들중 하나는 몇몇 클래스들의 소스 코드를 생성자가 아닌 setter로 구성되도록 수정하는 것이다.
생성자 기반 의존성 주입 말고 setter 기반 의존성 주입을 사용할 수도 있다. 정리하자면 추천하는 방식은 아니지만 setter 기반 의존성 주입을 사용하여 순환 의존성 문제를 해결할 수 있다.
순환 의존성이 발생하지 않는 경우와 다르게 발생하는 경우에는, 위의 클래스 A, B 예시처럼 한 Bean이 자신이 완전히 초기화 되기도 전에 다른 Bean에게 주입되도록 강요하게 된다.여러분은 Spring이 올바르게 작동한다고 신뢰할 수 있다. Spring은 존재하지 않는 Bean에 대한 참조 및 순환 의존성과 같은 구성 문제들을 IoC 컨테이너가 로드되는 시점에 발견할 수 있다. Spring은 프로퍼티들을 세팅하는 것과 의존성들을 해석(resolution)하는 것을 가능한 늦게(Bean이 실제로 생성될때) 한다. 이는 Spring IoC 컨테이너가 오류 없이 로드됬다 하더라도 나중에 예외(자신이나 자신의 의존성들을 생성하는데 문제가 있는 객체를 생성할 때)가 발생할 수 있다는 의미로도 해석할 수 있다. 이렇게 IoC 컨테이너가 지연된(delay) 예외를 발생시킬 가능성이 있기 때문에, ApplicationContext 인터페이스의 구현체의 기본 설정값이 pre-instatiate singleton bean(IoC 컨테이너가 로드되는 시점에 초기화됨)인 것이다. 이렇게 Bean들을 실제 필요한 시점 전에 생성하는 것은 메모리 관리 측면에서 효율적이지 못하지만, IoC 컨테이너가 나중에 발생시킬 가능성이 있는 문제들을 IoC 컨테이너가 생성되는 시점에서 미리 확인할 수 있다는 장점이 있다. 이러한 기본 값이 마음에 들지 않는다면 오버라이드(override)하여 Bean이 요청받았을때 생성되도록 변경할 수 있다.
순환 의존성이 없다고 가정하고, 1개 이상의 협력 Bean들이 의존관계에 있는 Bean에 주입된다고 하자. 그렇다면 이 협력 Bean들은 의존관계에 있는 Bean에 주입되기 전에 완전히 구성(configured) 되어야 한다. 예를 들어 Bean A 와 Bean B 총 2개의 Bean이 있고, Bean A가 Bean B에 의존성을 가지고 있다고 하자. setter 기반 의존성 주입을 사용한다고 한다면, Bean B는 Bean A 내의 setter 메소드를 호출하기 전에 완전히 구성되어야 한다.
Examples of Dependency Injection
아래는 setter 기반 의존성 주입을 위한 xml 기반 구성 메타데이터의 예시이다.
<bean id="exampleBean" class="examples.ExampleBean"> <!--중첩된 ref 요소를 이용한 setter 기반 의존성 주입--> <property name="beanOne"> <ref bean="anotherExampleBean"/> </property> <!--프로퍼티내 ref 속성을 이용한 setter 기반 의존성 주입--> <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; //<property name="beanOne"> // <ref bean="anotherExampleBean"/> //</property> public void setBeanOne(AnotherBean beanOne) { this.beanOne = beanOne; } //<property name="beanTwo" ref="yetAnotherBean"/> public void setBeanTwo(YetAnotherBean beanTwo) { this.beanTwo = beanTwo; } //<property name="integerProperty" value="1"/> public void setIntegerProperty(int i) { this.i = i; } }위의 자바 코드를 보면 xml 구성 메타데이터내 프로퍼티 name과
ExampleBean 클래스내 setter 메소드의 이름들이 매칭되는 것을 알 수 있다.
다음은 생성자 기반 의존성 주입을 사용하는 예시이다.
<bean id="exampleBean" class="examples.ExampleBean"> <!-- 중첩된 ref 요소를 사용한 생성자 기반 의존성 주입 --> <constructor-arg> <ref bean="anotherExampleBean"/> </constructor-arg> <!-- constructor-arg 요소내 ref 속성을 사용한 생성자 기반 의존성 주입 --> <constructor-arg ref="yetAnotherBean"/> <constructor-arg type="int" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/>다음은 위의 xml 기반 구성 메타데이터를 통해 생성자 기반 의존성 주입을 받는 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; } }xml 구성 메타데이터내 Bean 정의에 명시되있는 생성자 인수들은 ExampleBean 클래스 생성자로 전달된다.
다음은 정적 팩토리 메소드를 사용하는 의존성 주입의 예시이다.
<!--정적 팩토리 메소드 기반 의존성 주입 예제--> <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"/>다음은 위의 xml 구성 메타데이터에 대응되는 ExampleBean 클래스의 코드이다.
public class ExampleBean { // private 생성자 private ExampleBean(...) { ... } // 정적 팩토리 메소드 "createInstance(...)" // 이 메소드로 전달되는 인수들은 그들이 실제로 사용되는지와 상관 없이 // 이 메소드가 반환하는 Bean의 의존성들로 취급된다. public static ExampleBean createInstance ( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { ExampleBean eb = new ExampleBean (...); // 이하 생략.. return eb; } }위의 예제를 보면 알겠지만, 정적 팩토리 메소드로의 인수는 xml 기반 메타데이터내 <constructor-arg /> 요소에 의해 전달된다.
이는 생성자 기반 의존성 주입시에 사용한 요소와 똑같다.
정적 팩토리 메소드로 부터 반환되는 클래스의 타입은 해당 정적 팩토리 메소드를 포함하는 클래스의 타입과 달라도 상관 없다.
public class ExampleBean { <!-- 정적 팩토리 메소드를 포함하는 클래스의 타입과 정적 팩토리 메소드로부터 반환되는 클래스의 타입이 다름 --> public static AnotherBean createInstance (...) { ... } }인스턴스 팩토리 메소드의 경우는 정적 팩토리 메소드와 비슷하기 때문에 공식문서에서 따로 예시를 보여주지 않았다.
'JAVA > Spring' 카테고리의 다른 글
[Spring Core] 공식문서 공부 - 1.4 Dependencies (0) 2022.08.25 [Spring Core] 공식문서 공부 - 1.3.2 Instantiating Bean (0) 2022.08.22 [Spring Core] 공식문서 공부 - 1.3.1 Naming Beans (0) 2022.08.21 [Spring Core] 공식문서 공부 - 1.3 Bean Overview (0) 2022.08.21 [Spring Core] 공식문서 공부 - 1.2.3 Using the Container (0) 2022.08.21