생성자 주입 — 자바 편 15강의 "밖"이 여기였다
Article
5강 끝에서 스스로 문제를 제기했다.
Notifier n = ctx.getBean(Notifier.class);
new 는 지웠는데 getBean 이 생겼다. 자바 편 15강은 OrderService 가 EmailNotifier 라는 이름에 묶이는 것을 문제 삼았는데, 이제는 ctx 라는 것에 묶였다. 묶인 상대만 바뀌었을 뿐이다.
이번 강의에서 그것도 지운다.
자바 편 15강의 코드를 그대로 가져온다
기억을 되살리자. 15강의 OrderService 는 이렇게 생겼었다.
class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) {
this.notifier = notifier;
}
...
}
그리고 이렇게 썼다.
new OrderService(new EmailNotifier()).order();
밖에서 만들어서 넣어준다고 했다. 그 "밖"이 결국 우리 손이었다.
이제 표식을 하나 붙인다. 그게 전부다.
@Component
public class OrderService {
private final Notifier notifier;
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void order() {
System.out.println("주문을 처리했다");
notifier.send("주문이 완료됐습니다");
}
}
생성자 본문은 자바 편 15강과 똑같다. this.notifier = notifier; 한 줄. 우리가 추가한 것은 클래스 위의 @Component 뿐이다.
Notifier 를 어디서 구해오라는 코드가 없다. getBean 도 없다. new 도 없다. 그냥 생성자 매개변수에 타입이 적혀 있을 뿐이다.
돌려본다.
- ### OrderService 안에 들어온 것: EmailNotifier
- 주문을 처리했다
- 메일: 주문이 완료됐습니다
- ### 컨테이너의 Notifier 와 같은 객체인가: true
들어왔다. 아무도 넣으라고 시키지 않았는데 들어왔다.
컨테이너가 한 일
- 01
OrderService를 만들어야 한다@Component가 붙어 있으니 빈으로 만들어야 한다. 그런데 생성자를 보니 인자가 하나 필요하다. - 02
생성자가 무슨 타입을 요구하는지 읽는다
Notifier타입이다. 타입만 보면 된다. 이름은 안 봐도 된다.public OrderService(Notifier notifier) - 03
상자에서 그 타입의 빈을 찾는다
EmailNotifier가Notifier를 구현하고 있고@Component가 붙어 있다. 찾았다. - 04
찾은 것을 넣어서
new한다new OrderService(emailNotifier). 자바 편 15강에서 우리 손으로 치던 그 줄을 컨테이너가 대신 친다.
세 번째 줄의 출력도 짚고 넘어가자. OrderService 안에 들어온 객체가 컨테이너의 그 빈과 같은 객체다. == 가 참이다.
5강에서 빈은 하나뿐이라고 했다. 컨테이너는 EmailNotifier 를 새로 만들어 넣어준 게 아니라, 이미 상자에 있던 그 하나를 가리켜줬다.
이름이 사라졌다
OrderService 본문을 다시 훑어보자. EmailNotifier 라는 글자가 한 번도 안 나온다. Notifier 라는 타입만 있다.
그러면 구현체를 바꿔도 OrderService 는 모를 것이다. 정말 그런지 해보자.
EmailNotifier 에서 @Component 를 떼고, SmsNotifier 를 새로 만들어 거기에 붙인다.
public class EmailNotifier implements Notifier { // 표식을 뗐다
public void send(String msg) {
System.out.println("메일: " + msg);
}
}
@Component
public class SmsNotifier implements Notifier { // 표식을 옮겼다
public void send(String msg) {
System.out.println("문자: " + msg);
}
}
OrderService 는 한 글자도 안 건드렸다.
- ### OrderService 안에 들어온 것: SmsNotifier
- 주문을 처리했다
- 문자: 주문이 완료됐습니다
- ### 컨테이너의 Notifier 와 같은 객체인가: true
자바 편 15강에 이런 문장이 있었다.
OrderService코드는 한 글자도 안 바뀌었다. 카카오톡을 추가해도 안 바뀐다.
그때는 그 문장이 참이 되려면 누군가 밖에서 new OrderService(new SmsNotifier()) 라고 고쳐 써야 했다. 고칠 코드가 OrderService 바깥으로 옮겨갔을 뿐, 누군가는 고쳐야 했다.
이제는 아무도 안 고친다. 표식을 옮겼을 뿐이다.
둘 다 붙이면 어떻게 되나
당연한 질문이다. EmailNotifier 와 SmsNotifier 둘 다 @Component 를 붙이면?
컨테이너 입장에서 생각해보자. OrderService 의 생성자가 Notifier 하나를 달라고 하는데, 상자에는 Notifier 가 둘이다. 누구를 넣어야 하나.
- ***************************
- APPLICATION FAILED TO START
- ***************************
- Description:
- Parameter 0 of constructor in kr.dcodelab.bookstore.OrderService required a single bean, but 2 were found:
- - emailNotifier: defined in file [.../EmailNotifier.class]
- - smsNotifier: defined in file [.../SmsNotifier.class]
- Action:
- Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Spring 이 해법을 세 개나 알려준다. 4강에서 포트 충돌 때도 그랬다. 무엇이 잘못됐는지(Description)와 무엇을 하라는지(Action)를 나눠서 찍는다.
여기서 놓치면 안 되는 것이 하나 있다.
5강 — 빈이 없을 때
getBean 으로 꺼내다 실패
- 기동은 성공한다
- 서버가 뜬 뒤에
getBean을 부르는 순간 예외NoSuchBeanDefinitionException- 돌려봐야 안다
6강 — 넣을 것이 모호할 때
생성자 주입이 실패
- 기동 자체가 실패한다
APPLICATION FAILED TO START- 요청 한 번 못 받아본다
UnsatisfiedDependencyException- 뜨기도 전에 안다
새벽 세 시에 특정 요청이 들어왔을 때 처음 터지는 것과, 배포하자마자 안 뜨는 것. 어느 쪽이 나은지는 분명하다.
둘 중 하나를 고른다
Spring 이 알려준 대로 @Primary 를 붙여본다. "굳이 안 정해주면 이걸 써라"는 뜻이다.
@Component
@Primary
public class EmailNotifier implements Notifier { ... }
- ### OrderService 안에 들어온 것: EmailNotifier
- 메일: 주문이 완료됐습니다
SmsNotifier 도 여전히 @Component 인데, @Primary 가 붙은 쪽이 뽑혔다.
그런데 어떤 곳에서는 문자를 보내고 싶다면? 그때는 받는 쪽에서 콕 집어 말한다.
public OrderService(@Qualifier("smsNotifier") Notifier notifier) {
this.notifier = notifier;
}
"smsNotifier" 는 5강에서 본 그 이름이다. 클래스 이름의 첫 글자를 내린 것.
- ### OrderService 안에 들어온 것: SmsNotifier
- 문자: 주문이 완료됐습니다
- ### 컨테이너의 Notifier 와 같은 객체인가: false
@Qualifier 가 @Primary 를 이긴다.
마지막 줄이 false 인 것에 주목하자. ctx.getBean(Notifier.class) 는 여전히 @Primary 인 EmailNotifier 를 준다. 그런데 OrderService 는 SmsNotifier 를 들고 있다. 같은 타입이지만 다른 객체다. 한 줄이 두 사실을 동시에 증명한다.
| 우선순위 | 무엇 | 어디에 붙이나 |
|---|---|---|
| 1 | @Qualifier("이름") | 받는 쪽 생성자 매개변수 |
| 2 | @Primary | 주는 쪽 클래스 |
| 3 | 후보가 하나면 그것 | — |
| — | 후보가 둘인데 아무 표시도 없으면 | 기동 실패 |
왜 하필 생성자인가
필드에 바로 넣는 방법도 있다. 옛 예제에서 흔히 본다.
필드 주입
@Autowired 를 필드에
final을 못 붙인다 — 나중에 바뀔 수 있다new OrderService()가 가능해진다. 그런데 `notifier` 는 `null` 이다- 테스트에서 가짜를 넣으려면 리플렉션이 필요하다
- 생성자만 봐서는 무엇이 필요한지 모른다
생성자 주입
생성자 매개변수로
final을 붙일 수 있다 — 한 번 정하면 못 바꾼다- 인자 없이는 아예 못 만든다.
null인 채로 존재할 수 없다 - 테스트에서 그냥 넣어주면 된다
- 생성자만 보면 무엇이 필요한지 다 보인다
final 은 자바 편 13강에서 배웠다. 한 번 정하면 재대입이 안 된다. 생성자에서 딱 한 번 넣고 그 뒤로 아무도 못 바꾼다.
두 번째 줄이 특히 중요하다. null 인 채로 존재할 수 있는 객체를 만들지 않는다. 자바 편 20강은 없을 수 있다는 사실을 Optional 이라는 상자에 담아 타입에 적는 법이었다. 생성자 주입은 방향이 반대다. 애초에 null 인 상태가 만들어지지 못하게 막는다.
그리고 자바 편 15강에 이런 문장이 있었다.
테스트할 때는 진짜 메일을 보내지 않는 가짜
Notifier를 넣으면 된다.OrderService는 눈치채지 못한다.
생성자로 받기 때문에 가능한 일이었다. 23강에서 실제로 가짜를 넣어본다.
getBean 은 어디로 갔나
OrderService 안에는 getBean 이 없다. ctx 도 없다. 자기가 무엇을 필요로 하는지만 적어뒀고, 그것을 어떻게 구하는지는 모른다.
이게 자바 편 15강이 말한 "밖에서 만들어서 넣어준다"의 완성이다.
- 자바 편 초반new EmailNotifier() — 구현체 이름에 묶인다
- 자바 편 15강생성자로 받는다 — 밖에서 누군가 new 해야 한다
- 5강ctx.getBean() — 컨테이너에 묶인다
- 6강타입만 적는다 — 아무것에도 안 묶인다
다만 아직 main 에 getBean 이 하나 남아 있다. 누군가는 첫 단추를 꿰어야 하니까.
OrderService svc = ctx.getBean(OrderService.class);
svc.order();
9강부터 이것도 사라진다. 컨트롤러는 우리가 부르는 게 아니라 요청이 들어올 때 Spring 이 부르기 때문이다.
정리하면
필요한 것을 생성자에 타입으로 적어두면, 컨테이너가 찾아서 넣어준다.
- 01
생성자에 타입만 적는다
@Autowired도,getBean도 필요 없다. 생성자가 하나뿐이면 표식조차 생략된다.public OrderService(Notifier notifier) { this.notifier = notifier; } - 02
구현체를 바꿔도 받는 쪽은 안 바뀐다
@Component를 옮기기만 하면 된다.OrderService는EmailNotifier라는 이름을 모른다. - 03
후보가 둘이면 기동이 실패한다
돌려봐야 아는 게 아니라 뜨기도 전에 안다. 생성자 주입의 가장 큰 장점이다.
- 04
@Qualifier가@Primary를 이긴다주는 쪽이 기본값을 정하고, 받는 쪽이 필요하면 뒤집는다.
- 05
생성자 주입이면
final을 붙일 수 있다인자 없이는 객체가 만들어지지 않는다. null 인 채로 존재할 수 없다.
private final Notifier notifier;
여기까지 오면서 계속 미룬 질문이 하나 있다.
@Component 를 붙이면 빈이 된다고 했다. 그런데 Spring 은 그 표식이 붙은 클래스를 어떻게 찾아내는가. 우리 프로젝트에 클래스가 다섯 개뿐이라 다행이지, 수백 개면 어쩔 것인가. 폴더를 전부 뒤지나.
그리고 5강에서 흘린 말이 있다. "자바 편 21강에서 우리가 만들어본 그 리플렉션 러너와 같은 일이다."
7강에서 그 뒤지는 과정을 직접 본다. 우리가 자바 편에서 손으로 짜본 것과 정말 같은 일인지 확인한다.