빈과 컨테이너 — new 를 누가 대신 하는가
Article
4강 마지막에 던진 질문이다.
기동 로그에는 톰캣이 만들어졌다고 적혀 있고, DispatcherServlet 이 초기화됐다고 적혀 있다. 그런데 우리가 쓴 코드는 main 한 줄뿐이었다. new Tomcat() 도, new DispatcherServlet() 도 우리는 친 적이 없다.
누가 만들었나.
run 은 무언가를 돌려준다
4강에서 SpringApplication.run 이 곧바로 돌아온다는 것을 확인했다. 그때 이렇게만 적고 넘어갔다 — "돌려주는 값이 있다. 5강에서 그 값을 받아 쓴다."
그 값을 받아보자.
@SpringBootApplication
public class BookstoreApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(BookstoreApplication.class, args);
System.out.println("### 빈 개수: " + ctx.getBeanDefinitionCount());
}
}
- ### 빈 개수: 146
146개. 우리가 만든 건 하나도 없는데 객체가 146개 있다.
ApplicationContext 라는 이 물건이 그것들을 들고 있다. 이걸 컨테이너라고 부른다. 상자다. 그리고 상자 안에 든 객체 하나하나를 빈이라고 부른다.
- 만든다필요한 객체를 대신 new 한다
- 보관한다이름표를 붙여 상자에 넣는다
- 꺼내준다달라고 하면 준다
상자 안을 들여다본다
이름을 몇 개만 뽑아봤다.
Arrays.stream(ctx.getBeanDefinitionNames())
.forEach(System.out::println);
- bookstoreApplication
- dispatcherServlet
- dispatcherServletRegistration
- tomcatServletWebServerFactory
- tomcatWebServerFactoryCustomizer
- org.springframework.boot.tomcat.autoconfigure.TomcatServletWebServerAutoConfiguration
첫 줄을 보라. bookstoreApplication. 우리가 쓴 그 클래스도 빈이다. 자기가 만든 상자에 자기가 들어가 있다.
dispatcherServlet 도 있다. 4강 기동 로그의 마지막 줄에 Initializing Spring DispatcherServlet 'dispatcherServlet' 이라고 찍혔던 그것이다. 이름이 정확히 같다.
정말 꺼낼 수 있는지 확인해보자.
DispatcherServlet ds = ctx.getBean(DispatcherServlet.class);
System.out.println("### 우리가 new 한 적 없는 것: " + ds.getClass().getName());
- ### 우리가 new 한 적 없는 것: org.springframework.web.servlet.DispatcherServlet
객체가 손에 잡힌다. new 를 친 사람은 우리가 아니다.
우리 것도 넣어본다
자바 편 15강의 그 코드를 그대로 가져온다. 기억날 것이다.
public interface Notifier {
void send(String msg);
}
구현체에 표식을 하나 붙인다.
@Component
public class EmailNotifier implements Notifier {
public void send(String msg) {
System.out.println("메일: " + msg);
}
}
@Component. 이게 전부다. 자바 편 21강에서 배운 대로, 이 표식은 아무 일도 하지 않는다. 실행되지도 않는다. 그저 누군가 읽으라고 붙여둔 것이다.
읽는 쪽이 있는지 확인한다.
Notifier n = ctx.getBean(Notifier.class);
System.out.println("### a 의 실제 타입: " + n.getClass().getSimpleName());
System.out.println("### 빈 이름: " + String.join(", ", ctx.getBeanNamesForType(Notifier.class)));
- ### a 의 실제 타입: EmailNotifier
- ### 빈 이름: emailNotifier
두 가지를 짚어야 한다.
하나. getBean(Notifier.class) 라고 인터페이스로 달라고 했는데 EmailNotifier 가 나왔다. 자바 편 15강의 다형성 그대로다. 컴파일러는 Notifier 로만 알고, 상자 안에 든 진짜 물건은 EmailNotifier 다.
둘. 이름표가 emailNotifier 다. 우리가 지어준 적이 없다. 클래스 이름의 첫 글자를 소문자로 바꾼 것이 기본 이름이다.
두 번 꺼내면 두 개인가
여기서 자바 편 4강이 돌아온다. == 는 주소를 본다.
Notifier a = ctx.getBean(Notifier.class);
Notifier b = ctx.getBean(Notifier.class);
System.out.println("### 두 번 꺼냈다: a == b ? " + (a == b));
EmailNotifier c = new EmailNotifier();
EmailNotifier d = new EmailNotifier();
System.out.println("### 직접 new 했다: c == d ? " + (c == d));
- ### 두 번 꺼냈다: a == b ? true
- ### 직접 new 했다: c == d ? false
직접 new 하면 부를 때마다 새 객체다. 자바 편 내내 그랬다.
그런데 컨테이너는 몇 번을 물어봐도 같은 객체를 준다. 주소가 같다.
getBean 은 만들지 않는다. 이미 만들어둔 것을 가리킨다.
- a
- 주소
- → EmailNotifier 객체 하나
- b
- 주소
- → 같은 객체
- c
- 주소
- → new 로 만든 객체
- d
- 주소
- → 또 다른 객체
컨테이너는 기동할 때 EmailNotifier 를 딱 한 번 만들어 상자에 넣어둔다. 그 뒤로는 달라고 할 때마다 그 하나를 가리켜준다. 이걸 싱글턴이라고 한다.
기동 로그의 그 1.44초 가 이 일을 하고 있었다. 146개를 만들어 상자에 넣는 시간이다.
표식을 떼면
@Component 를 지우고 그대로 돌려봤다.
- Exception in thread "main"
- org.springframework.beans.factory.NoSuchBeanDefinitionException:
- No qualifying bean of type 'kr.dcodelab.bookstore.Notifier' available
- at kr.dcodelab.bookstore.BookstoreApplication.main(BookstoreApplication.java:11)
"그런 타입의 빈이 없다." 클래스는 멀쩡히 있다. 컴파일도 됐다. 다만 상자에 안 들어갔을 뿐이다.
제어가 뒤집혔다
자바 편에서 객체를 얻는 방법은 하나였다. 필요하면 new 한다.
자바 편
내가 만든다
- 필요할 때 내가
new한다 - 무엇을 만들지 내 코드가 정한다
new EmailNotifier()라고 적는 순간, 그 이름에 묶인다- 바꾸려면 코드를 고친다
Spring
컨테이너가 만든다
- 기동할 때 컨테이너가 미리
new해둔다 - 무엇을 만들지 표식을 보고 정한다
- 내 코드에는
Notifier라는 타입만 적힌다 - 바꾸려면 표식을 옮긴다
거창한 이름이 붙어 있지만 방금 본 것이 전부다. 내가 new 하던 것을 남이 대신 new 한다.
그래서 컨테이너를 IoC 컨테이너라고도 부른다.
아직 절반이다
자바 편 15강을 다시 펴보자. 거기 이렇게 적혀 있었다.
OrderService는 필요한 것을 스스로 만들지 않고 생성자로 받았다. 그래서 무엇을 받을지 밖에서 정할 수 있다.
그리고 이렇게 썼다.
new OrderService(new EmailNotifier()).order();
밖에서 만들어서 넣어준다고 했는데, 그 "밖"이 결국 우리 손이었다. 누군가는 new 를 쳐야 했다.
이제 그 밖의 이름을 안다. 컨테이너다.
그런데 아직 이상하다. 지금 우리 코드는 이렇게 생겼다.
Notifier n = ctx.getBean(Notifier.class);
new 는 사라졌는데 getBean 이 생겼다. 컨테이너에게 손을 내밀어 직접 꺼내고 있다. 이러면 이번엔 ctx 라는 것에 묶인다. 자바 편 15강이 EmailNotifier 라는 이름에 묶이는 것을 문제 삼았는데, 이름만 바뀌었을 뿐이다.
진짜 답은 getBean 조차 부르지 않는 것이다. 6강에서 본다.
정리하면
컨테이너는 객체를 대신 만들어 보관하고, 달라고 하면 꺼내준다. 그 객체를 빈이라고 부른다.
- 01
run은 컨테이너를 돌려준다기동이 끝나면 그 안에 빈 146개가 들어 있다. 톰캣도, DispatcherServlet 도, 우리 클래스도.
ApplicationContext ctx = SpringApplication.run(BookstoreApplication.class, args); - 02
@Component는 상자에 넣어달라는 표식이다아무 일도 하지 않는다. 누군가 읽어야 뜻이 생긴다. 안 붙이면 NoSuchBeanDefinitionException 이다.
@Component public class EmailNotifier implements Notifier { } - 03
빈 이름은 클래스 이름의 첫 글자를 내린 것이다
EmailNotifier→emailNotifier. 지어준 적 없는데 붙어 있다. - 04
빈은 하나뿐이다
두 번 꺼내도
==가 참이다. 직접 new 하면 거짓이다. 그래서 빈에 상태를 두면 안 된다.ctx.getBean(Notifier.class) == ctx.getBean(Notifier.class) // true - 05
제어의 역전 — 내가 아니라 남이 new 한다
그게 IoC 다. 이름만 거창하다.
다음 강의에서 getBean 을 지운다.
OrderService 가 Notifier 를 어디서 얻을까. 컨테이너에게 물어보지도 않는다. 그냥 생성자에 적어두면, 컨테이너가 알아서 넣어준다. 자바 편 15강이 예고해둔 그 문장이 6강에서 완성된다.