직역하면 제어 반전이다. 사용자가 짠 구체적인 코드가 framework 등에서 flow of control을 받는 것을 의미한다.
보통 우리가 프로그램을 짜면 우리가 작성한 프로그램이 있고, 그 프로그램이 사용하는 라이브러리가 있다. 우리 프로그램이 라이브러리(framework)에 있는 함수를 호출하면 거기로 control이 넘어간다. 이를 control flow가 사용자 코드에서 framework으로 넘어간다고 생각할 수 있다.
이것의 반대 상황, 즉 control flow가 framework에서 사용자 code로 넘어간다는 것을 Inversion of Control이라고 한다.
이럴 때 프로그램은 어떻게 동작할까? 처음에 프레임워크의 코드가 control flow를 쥐고 있고 특정 상황에서 우리가 작성한 코드로 넘어가는 방식을 취한다.
그럴려면 프레임워크가 우리의 코드를 추적할 수 있어야 우리 코드로 control을 넘길 수 있다. 마치 우리가 라이브러리를 모종의 방법으로 추적할 수 있어야 라이브러리로 control을 넘길 수 있는 것처럼 말이다. 그래서 관련 추상화 및 방식들을 프레임워크에서 제공한다. 아니면 프레임워크에서 제공하는 인식 기능을 활용해서 우리가 처음부터 직접 만든 class를 인지하게 하는 것도 가능하다.
이 뭔가 오묘한 방식의 이점은 다음과 같다.
class A {
...
}
class B {
A obj = new A();
}
애플리케이션을 만들다 보면은 특정 software이 본인이 의도한 기능을 제공하기 위해 다른 기능을 제공하는 software에 의존하는 경향이 많이 있다. 핵심적인 기능을 담당하는 software은 다양한 다른 핵심적인 기능, 혹은 부수적인 기능을 담당하는 software들이 그러다 보면 이들은 각기 다른 요소랑 의존 관계를 형성하게 될 확률이 높다.
위 의존관계의 경우 B에서 A에 해당하는 object를 '직접' 생성하고 있다. 하지만 외부에서 B를 위한 A class instance를 '주입'하는 것도 가능하다. 이를 injection이라고 한다.
밑과 같이 pseudocode를 만들어보자.
class A {
...
}
class B{
A obj;
injection(A obj) {
this.obj = obj;
}
}
injection
이라는 method는 주입을 위한 툴이다. 이제 밑과 같이 코드를 짜면 B에 대해 A의 instance를 주입하는 것이다. 이를 종합해 Dependency Injection이라고 한다.B bInst = new B;
bInst.injection(new A)
interface
를 활용하는 것이다.관련해서 좋은 글이 있으며 이 글의 예시를 참고해서 설명하니 이해가 잘 안되면 이 링크의 소스코드 확인 바람.
A
랑 B
라는 class가 있는데, A
가 하위 module, B
가 상위 module이라고 해보자.
B
랑 A
가 의존 관계라고 하고, 둘을 연결짓는 추상화 (위 링크의 경우, Interface
)를 I
라고 해보자.
B
는 저 I
에 해당해는 객체를 보유하도록 설계되며, I
의 구현체를 받을 수 있는 방식을 어떻게든 만든다. 대표적인 방식은 앞의 DI 설명때 나온 구현체를 method를 통해 받는 것.
A
는 I
를 실제로 구현한다. (내부 구현)
B
가 A
랑 의존관계를 갖도록 디자인할거면 저 I
의 실제 구현으로 들어가는 것이 A
의 객체이도록 한다. 즉 user level에서 보여질 기능, 즉 제어 주체를 외부에서 넣는다.
이러면 A
랑 B
의 의존관계가 중간의 추상화에 의해 독립된다. 즉 B
입장에서 제어주체는 임의의 추상화로 보이게, 관련해서 오류가 나올 때도 추상화 부분에서 오류가 나오기 때문에 분석이 좀 더 쉬워진다.
IoC를 활용하는 프레임워크들은 보통 IoC container이라는 것을 보유한다. IoC container이란 위의 IoC를 적용받는 객체들이 언제 생성되고 언제 소멸할지를 관리한다. 또 앞의 DI 및 DIP 형식으로 IoC를 관리할 수 있도록 객체의 의존성 및 의존성 주입을 총괄하는 녀석이다. 한마디로 IoC 유지 총괄자다.
저번 글에서 Spring도 IoC를 활용한다고 했다. 그리고 실제로 IoC container도 보유한다. 이에 해당하는 interface가 바로 ApplicationContext
다.
IoC container, 그러니까 Spring에서 ApplicationContext
가 관리하는 객체들을 Spring에서는 Bean이라고 부른다.
Spring은 이 ApplicationContext
의 여러 구현체를 제공한다. AnnotationConfigApplicationContext
, ClassPathXmlApplicationContext
, FileSystemXmlApplicationContext
, WebApplicationContext
등이 대표적. 마지막은 관련 인터페이스이긴 하다.
IoC container이 Bean을 생성하려면 설정 정보를 읽어야 한다. 위의 구현체들도 이 설정 정보들을 어떻게 읽느냐에 대해서 차이를 가지는 것이다.
AnnotationConfigApplicationContext
: 설정 정보를 @Configuration
과 같은 annotation을 통해 파악한다.ClassPathXmlApplicationContext
: 설정 정보를 Java class path에 있는 xml 파일을 읽어서 파악한다.FileSystemXmlApplicationContext
: 설정 정보를 URL 혹은 파일 시스템에 대한 경로를 활용해가지고 파악한다.WebApplicationContext
: 웹 애플리케이션 용도의 configuration context다. 가장 큰 차이점은 앞의 3개와 같은 일반 application용 ApplicationContext
는 application 하나당 context가 하나지만, WebApplicationContext
는 DispatcherServlet별로 context를 하나씩 가지는 것도 가능하다.위의 class의 생성자들을 이용해서 직접 ApplicationContext
를 생성하는 것이 가능하다. 생성하는 그 즉시 Spring IoC container이 그걸 기반으로 bean을 관리하기 시작한다.
ApplicationContext context
= new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext
를 예로 들면, 이 녀석에게 @Configuration
이 달린 class들을 전부 전달하면 IoC container이 거기 안의 @Bean
과 관련된 annotation이 뭐가 있는지를 파악한다. 전부 파악이 되면 관련 bean들을 전부 생성하며, 이들 간의 관계도 고려해서 DI까지 직접 해준다. 그리고 언제 소멸이 될지까지도 관리를 한다.
이 때 Spring의 IoC container은 여러가지 방식으로 bean을 초기화 및 주입한다.
생성자를 호출해가지고 bean을 주입하는 방식이다.
AnnotationConfigApplicationContext
를 활용한다고 가정시 밑과 같이 코드를 짜면 Item
과 Store
에 해당하는 bean을 하나씩 만들고 Store
bean에 방금 만든 Item
bean이 들어간다.
생성되는 bean에 이름 부여도 되는데, 안할 경우 생성에 사용한 생성자 이름이 bean의 이름이 된다. (
item1
,store
)
@Configuration
public class AppConfig {
@Bean
public Item item1() {
return new ItemImpl1();
}
@Bean
public Store store() {
return new Store(item1());
}
}
<bean id="item1" class="org.baeldung.store.ItemImpl1" />
<bean id="store" class="org.baeldung.store.Store">
<constructor-arg type="ItemImpl1" index="0" name="item" ref="item1" />
</bean>
여기서 잠시 다른 얘기를 좀 해보겠다. bean 생성에서 한가지 유의할게 있는데, 바로 bean의 scope. 여기서는 Singleton과 Prototype에 대해 얘기해보겠다.
명시가 없으면 singleton scope로 보통 bean을 생성한다. 이 경우 해당 종류의 bean은 딱 하나만 유지가 된다. 객체가 하나라는 것이다. 그래서 관련 bean을 여러 곳에서 주입을 받을 때 다 동일한 bean임이 보장된다.
반대로 prototype으로 scope를 설정할 수 있다. 이 경우 해당 bean에 대한 주입을 요청하는 bean들 마다 해당 bean의 다른 instance를 보유하게 된다. 즉 여러 곳에 주입을 받은 bean들이 실제로 다 서로 다르다는 것이다. 이 bean은 더이상 사용이 안될 때 garbage collector에 의해 제거가 된다.
@Scope
라는 annotation을 관련 @Configuration
포함 annotation이 달린 class에 달면 scope 변경이 가능하다.
또 다른 bean 주입 방식. argument가 없는 생성자나 static factory method (이게 뭔지 몰라도 일단 넘어가자)를 호출해서 bean이 instantiate 된 다음에 setter을 통해 instantiate된 bean을 주입하는 것이다.
annotation 기반으로 한다면 밑과 같이 하면 Store
에 해당하는 bean에 Item
에 해당하는 bean을 instantiate해 주입할 수 있다.
@Bean
public Store store() {
Store store = new Store();
store.setItem(item1());
return store;
}
<bean id="store" class="org.baeldung.store.Store">
<property name="item" ref="item1" />
</bean>
@Autowired
를 사용하면 된다.public class Store {
@Autowired
private Item item;
}
<bean
id="indexService"
class="com.baeldung.di.spring.IndexService" />
<bean
id="indexApp"
class="com.baeldung.di.spring.IndexApp" >
<property name="service" ref="indexService" />
</bean>
<bean
id="indexApp"
class="com.baeldung.di.spring.IndexApp">
<constructor-arg ref="indexService" />
</bean>
이 때 한가지 의문이 들 수 있는게, 저 item
field는 private다. 접근자도 주어져있지 않는데 어떻게 setter이나 생성자가 주어지지 않아도 IoC가 저기에 bean을 주입하는게 가능할까?
가능하다. Java의 reflection이라는 기능 덕분이다. 이에 대한 자세한 내용은 이 글 참고
이 기능을 사용한다는 것 때문에 Field-based DI를 추천하지 않는다. 이유는 생성자 기반이나 setter 기반보다 비용이 더 들기 때문. 뭐 그 외에도 너무 쉽게 dependency를 주입하는게 가능해서 SOLID의 S인 Single Responsibility Principle을 어기기 쉽다는 것도 있다만.
앞과 같이 Spring에서 알아서 적절히 주입을 해주는 것을 Wiring이라고 한다. 이 Wiring 방식이 앞과 같은 한가지 방식만 있는게 아니다. xml 형태의 configuration 파일을 활용해 어떤 방식으로 wiring을 할지 지정하는게 가능한데 밑의 종류가 가능하다.
예시로 byType의 경우 다음과 같이 하면 된다.
<bean id="store" class="org.baeldung.store.Store" autowire="byType"> </bean>
다른 예시로 byName의 경우 다음과 같이 하면 된다.
코드에서도 위의 종류들을 다 사용하는게 가능하다.
no의 경우 그냥 @Autowire
을 안 사용하면 된다.
name의 경우 정확히 대응되는건 없는데, @Qualifier
을 활용해서 유사한 효과를 내는게 가능하다.
public class Store {
@Autowired
@Qualifier("item1")
private Item item;
}
앞에서 소개한 예제가 type 기반 autowire이다.
마지막으로 생성자 기반 autowire은 밑과 같이 한다.
public class Store {
private Item item;
@Autowired
public Store(Item item) {
this.item = item;
}
}
lazy-init
을 설정하면 된다.<bean id="item1" class="org.baeldung.store.ItemImpl1" lazy-init="true" />
@Lazy
라는 annotation을 사용하면 된다.@Configuration
public class AppConfig {
@Bean
@Lazy
public Item item1() {
return new ItemImpl1();
}
}
application.properties
에 spring.main.lazy-initialization
을 true
로 설정하면 된다. application.yml
도 비슷한 방식.