학습 자료

빈과 컨테이너 — 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 라는 이 물건이 그것들을 들고 있다. 이걸 컨테이너라고 부른다. 상자다. 그리고 상자 안에 든 객체 하나하나를 이라고 부른다.

컨테이너가 하는 일은 셋뿐이다
  1. 만든다필요한 객체를 대신 new 한다
  2. 보관한다이름표를 붙여 상자에 넣는다
  3. 꺼내준다달라고 하면 준다
이 셋이 프레임워크의 심장이다. 나머지는 전부 이 위에 얹혀 있다.

상자 안을 들여다본다

이름을 몇 개만 뽑아봤다.

Arrays.stream(ctx.getBeanDefinitionNames())
    .forEach(System.out::println);
146개 중 눈에 띄는 것들
  • 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
주소
→ 또 다른 객체
a 와 b 는 같은 곳을 가리키고, c 와 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 라는 타입만 적힌다
  • 바꾸려면 표식을 옮긴다
객체를 만들 권한이 내 코드에서 프레임워크로 넘어갔다. 이 뒤집힘을 제어의 역전(IoC)이라고 부른다.

거창한 이름이 붙어 있지만 방금 본 것이 전부다. 내가 new 하던 것을 남이 대신 new 한다.

그래서 컨테이너를 IoC 컨테이너라고도 부른다.

아직 절반이다

자바 편 15강을 다시 펴보자. 거기 이렇게 적혀 있었다.

OrderService필요한 것을 스스로 만들지 않고 생성자로 받았다. 그래서 무엇을 받을지 밖에서 정할 수 있다.

그리고 이렇게 썼다.

new OrderService(new EmailNotifier()).order();

밖에서 만들어서 넣어준다고 했는데, 그 "밖"이 결국 우리 손이었다. 누군가는 new 를 쳐야 했다.

이제 그 밖의 이름을 안다. 컨테이너다.

그런데 아직 이상하다. 지금 우리 코드는 이렇게 생겼다.

Notifier n = ctx.getBean(Notifier.class);

new 는 사라졌는데 getBean 이 생겼다. 컨테이너에게 손을 내밀어 직접 꺼내고 있다. 이러면 이번엔 ctx 라는 것에 묶인다. 자바 편 15강이 EmailNotifier 라는 이름에 묶이는 것을 문제 삼았는데, 이름만 바뀌었을 뿐이다.

진짜 답은 getBean 조차 부르지 않는 것이다. 6강에서 본다.

정리하면

컨테이너는 객체를 대신 만들어 보관하고, 달라고 하면 꺼내준다. 그 객체를 빈이라고 부른다.

다시 짚어보기
  1. 01

    run 은 컨테이너를 돌려준다

    기동이 끝나면 그 안에 빈 146개가 들어 있다. 톰캣도, DispatcherServlet 도, 우리 클래스도.

    ApplicationContext ctx = SpringApplication.run(BookstoreApplication.class, args);
  2. 02

    @Component 는 상자에 넣어달라는 표식이다

    아무 일도 하지 않는다. 누군가 읽어야 뜻이 생긴다. 안 붙이면 NoSuchBeanDefinitionException 이다.

    @Component
    public class EmailNotifier implements Notifier { }
  3. 03

    빈 이름은 클래스 이름의 첫 글자를 내린 것이다

    EmailNotifieremailNotifier. 지어준 적 없는데 붙어 있다.

  4. 04

    빈은 하나뿐이다

    두 번 꺼내도 == 가 참이다. 직접 new 하면 거짓이다. 그래서 빈에 상태를 두면 안 된다.

    ctx.getBean(Notifier.class) == ctx.getBean(Notifier.class)   // true
  5. 05

    제어의 역전 — 내가 아니라 남이 new 한다

    그게 IoC 다. 이름만 거창하다.

다음 강의에서 getBean 을 지운다.

OrderServiceNotifier 를 어디서 얻을까. 컨테이너에게 물어보지도 않는다. 그냥 생성자에 적어두면, 컨테이너가 알아서 넣어준다. 자바 편 15강이 예고해둔 그 문장이 6강에서 완성된다.

빈과 컨테이너 — new 를 누가 대신 하는가 — 디코드랩(DCODELAB)