Spring Framework의 IoC 컨테이너 / 빈(Bean) / DI 방식 정리
https://docs.spring.io/spring/docs/5.1.x/spring-framework-reference/core.html
spring core의 공식 docs. 웬만큼 궁금한건 여기 다 나와 있다.
“컨테이너란 당신이 작성한 코드의 처리과정을 위임받은 독립적인 존재” 와 같은 추상적인 표현을 얘기하는 블로그와 책은 많으니까, 그 동안의 프레임워크 사용 경험을 바탕으로 공식 docs를 읽고 이해한 바를 구체적인 관점에서 정리해보았다.
Spring’s IoC 컨테이너 (Inversion of Control container)
일반적인 프로그램을 작성할 때는, 내가 진입점을 컨트롤 할 수 있기 때문에(main함수) 프로그램의 진행 흐름을 컨트롤 할 수 있다. 그러나 프레임워크를 사용할 때는, 진입점에서 프레임워크를 실행하는 작업만 수행하고 내가 작성한 코드를 따로 호출하지 않는다.
1
2
3
4
5
6
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
main 함수에서 그냥 run()만 불렀을 뿐이고 내가 작성한 코드를 호출하는 부분이 없다. 그럼 내가 작성한 코드(객체)는 언제 생성/초기화/실행/소멸하는가? 이걸 알아서 처리해주는게 프레임워크의 컨테이너다.
- 서버가 실행되었을 때
- 유저의 요청이 들어왔을 때
- 스케줄링 일정에 따라 일정한 주기가 도래했을 때
등등 다양한 상황에서 스프링 컨테이너가 “알아서” 내가 작성한 코드를 실행해 준다.
예를 들어, 유저의 요청이 들어오면 알아서 내가 작성한 Controller 메서드를 실행하고, 그로부터 만들어지는 응답을 내려주는 동작을 대신 해주는 것이다.
가령 프레임워크 없이, 간단한 raw webserver를 socket을 이용해 직접 만든다고 생각해보면
1
2
3
4
5
main()
while loop 돌면서 HTTP request가 들어왔는지 계속 체크하고,
들어왔다면
path가 /1이면 a객체 만들고 a.serve()
path가 /2이면 b객체 만들고 b.serve()...
즉, main에서 시작된 실행 흐름을 직접 while 돌면서 제어해야 할 것이다.
조금 더 요청이 많아지면 스레드를 여러 개로 빼거나, 비동기 처리를 해야 하는 등 복잡한 일거리가 생긴다.
그러나 스프링에서는 main에서 시작된 실행 흐름에 직접 관여하지 않는다. 그냥 run()호출하고 끝이다.
즉, 어떤 요청이 들어왔을 때 어떤 객체를 어느 시점에 생성하고 어떻게 사용하며 조작할 것인가? 에 대한 처리를 컨테이너가 대신 수행하게 된다.
이 덕분에, 개발자는 간단한 Configuration으로 컨테이너에게 동작을 지시하는 정도로, 비즈니스와 별로 관련이 없는 이러한 작업들을 컨테이너에게 위임할 수 있고 따라서 개발자가 비즈니스 로직을 작성하는데 집중할 수 있다. (그 외에도 물론 몇 가지 장점들이 있다.)
그리고 이처럼 프로그램의 실행 흐름이나 객체의 생명 주기를 개발자가 직접 제어하는게 아니라, 컨테이너로 제어권이 넘어가는 것을 Inversion of Control (IoC) 라고 한다. 그래서 IoC container라고 부르는 것이다.
Bean
위에서 컨테이너가 객체의 life-cycle을 관리한다고 했다. 이렇게 컨테이너가 관리하는 객체들을 빈이라고 부른다. 빈은 기본적으로 싱글턴이다.
다음 네 가지 애너테이션을 사용하면 해당 클래스를 자동으로 빈으로 등록해준다.
1
2
3
4
@Controller // Persentation layer에서 Controller임을 명시
@Service // Business layer에서 Service를 명시
@Repository // Persistence layer에서 DAO를 명시
@Component // 기타 자동 등록하고 싶은 것. 사실 위 3개는 Component의 use-case에 따른 specializations이다.
그 외 @Bean
이라는 애너테이션도 있는데, 이는 내가 직접 작성한 클래스가 아니라 외부 라이브러리의 객체를 빈으로 만들고 싶을 때 사용한다. 외부 라이브러리의 클래스에 애너테이션을 직접 붙일 수는 없으니 다음과 같이 객체를 반환하는 메서드에 붙여서 사용한다.
@Configuration
컴포넌트 내부에서 @Bean을 정의하는 경우와, 그냥 java @Component
내부에서 @Bean을 정의하는 경우의 동작이 조금 다르니 자세한 사항은 @Bean의 java docs를 읽어보면 된다.
1
2
@Bean
public ObjectMapper objectMapper() { return new ObjectMapper(); }
멀티 모듈로 분리되어 있는 경우, 어떤 클래스를 특정 모듈에서만 Bean으로 생성하고 싶은 상황 등에서 유용하다.
DI(Dependency Injection)
- [Coding/CodingNote] - 의존성 주입(DI, Dependency Injection)이란?
- 스프링에서는 빈의 생성 소멸 등 생명주기를 컨테이너가 관리하며, 필요로 하는 곳에 알아서 DI해준다.
- 이 때 필요로 하는 곳이란, 해당타입의 객체를 사용하는 곳 을 의미한다. (기본적으로 이름이 아니라 type 기반 매칭이다)
- 그냥 해당 타입의 객체를 사용하면 될까? 아니다. 컨테이너에게 이 빈이 필요할 것이니 DI 해달라고 명시해야 한다. 명시하는 방법은 크게 두 가지다.
- Constructor-based DI
- Setter-based DI
Field-based DI (@Autowired)요즘은 잘 사용하지 않는다.
Constructor-based DI
[!info] Constructor-based DI is accomplished by the container invoking a constructor with a number of arguments, each representing a dependency.
그냥 생성자에서 받는 방식이다. 즉, SimpleMovieLister
를 생성할 때, 컨테이너가 알아서 생성자에 MovieFinder
객체를 넣어주면서 생성하는 방식이다.
(이 때 final
로 설정하는 것이 좋다.) 생성자 방식은 몇 가지 장점이 있다.
final
이라 immutable이다.- final이라 null이 들어갈 때 compile time에 경고가 발생한다
- 유닛 테스트가 수월하다
- 생성자 파라미터를 통해 의존 관계를 바로 파악 가능하다.
1
2
3
4
5
6
7
8
9
10
11
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...
}
[!tip] lombok을 사용한다면 생성자를 직접 만들지 않고,
@RequiredArgsConstructor
로 대체 가능해 깔끔하다.
Setter-based DI
[!info] Setter-based DI is accomplished by the container calling setter methods on your beans after invoking a no-argument constructor or a no-argument static factory method to instantiate your bean.
즉 컨테이너가 setter method를 불러주면서 주입해주는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
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...
}
Constructor 방식과 Setter 방식 중 반드시 하나만 골라서 써야 하는 것은 아니다. ApplicationContext는 일부 dependency는 Constructor 접근을 통해 주입하고, 나머지에 대해서는 Setter 방식으로 주입하는 것도 지원한다.
그리고 두 방식 중 어떤 것이 좋은가?에 대해서는 다음과 같이 안내하고 있다.
[!tip] Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.
이유가 궁금하다면 공식 docs 참고. 좋은 내용이다.
근데 여기서 한 가지 문제가 생긴다. 자바의 다형성(polymorphism)을 생각해 보자.
스프링 컨테이너는 타입을 기반으로 어떤 빈을 DI 해주어야 할지를 결정한다고 했다.
만약 MovieFinder
가 인터페이스이고 && 받는 쪽에서는 MovieFinder
타입 빈을 요구하고 있고 && MovieFinder
타입 빈이 2개 이상이라면?
=> DI 후보 빈이 2개 이상이라, 컨테이너는 어떤 빈을 DI 해주어야 할지를 결정할 수 없다.
어떤 빈을 주입할지가 명확하지 않은 상황에서는 사용할 수 없다.
- https://www.baeldung.com/spring-autowire#disambiguation
- 결국 어떤 빈을 넘길지를 개발자가 직접 설정해주어야 하는데,
@Qualifier
애너테이션을 통해서 타입에 더해 이름을 추가로 사용하여 어떤 bean을 DI 받을지 명시할 수 있다. - Spring uses the bean’s name as a default qualifier value. 즉, 타입이 같은 빈이 2개 이상 있다고 하여도, 이름이 다르다면 해당 이름으로 DI 받을 수 있다. 따라서, 변수 이름을 bean 이름과 동일하게 가져간다면 대부분의 상황에서는 굳이
@Qualifier
애너테이션을 사용하지 않아도 bean을 1개로 특정 할 수 있다.
[!tip] 예전에는 XML base config를 많이 사용해서, 이를 XML에 명시했으나
요즘에는 Java-based Container Configuration, Annotation-based Container Configuration를 많이 사용한다.