이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Spring › IoC 컨테이너와 DI
스프링 컨테이너와 빈
new를 쓰지 않았는데 객체는 누가 만들어 넣어 주나
주문 서비스가 할인 정책 객체를 쓰려면 누군가 그 객체를 만들어야 합니다. 직접 new로 만들면 어떤 구현을 쓸지가 주문 서비스의 코드에 박힙니다. 스프링에서는 컨테이너가 객체(빈)를 만들고, 필요한 곳에 골라서 넣어 줍니다.
실행으로 대조 · 스프링 부트 4.1.1에서 서른 가지 조합을 실행해 대조
이 실습에 나오는 개념
- IoC(제어의 역전)
- 객체의 생성과 객체 사이의 연결을 개발자의 코드가 아니라 프레임워크(컨테이너)가 맡는 설계 원칙이다. 일반적인 프로그램에서는 내 코드가 필요한 객체를 직접 만들고 호출하지만, IoC에서는 컨테이너가 객체를 만들어 필요한 곳에 넣어 주고 적절한 시점에 내 코드를 호출한다. 제어의 주체가 뒤바뀌었다는 뜻에서 붙은 이름이다.
- 스프링 빈(Bean)
- 스프링 IoC 컨테이너가 생성하고 관리하는 객체다. 개발자가 new로 직접 만든 객체와 달리 컨테이너가 의존성을 주입하고 초기화와 소멸을 관리하며, 프록시를 통해 트랜잭션이나 AOP 같은 부가 기능을 적용할 수 있다.
- @Configuration과 @Bean
- @Configuration은 빈 정의를 담은 설정 클래스임을 나타내고, 그 안의 @Bean 메서드가 반환하는 객체가 빈으로 등록된다. 직접 수정할 수 없는 외부 라이브러리의 클래스를 빈으로 만들거나 생성 과정을 직접 제어해야 할 때 쓴다. 내가 작성한 클래스는 보통 @Component 계열을 붙여 등록한다.
- @Primary와 @Qualifier
- 같은 타입의 빈이 둘 이상일 때 어느 것을 주입할지 정하는 두 애너테이션이다. 스프링 컨테이너는 주입할 빈을 타입으로 찾는데, 맞는 빈이 여럿인데 어느 것을 쓸지 정할 단서(@Primary, @Qualifier, 이름)가 없으면 NoUniqueBeanDefinitionException을 던져 애플리케이션이 뜨지 않는다. @Primary는 빈을 등록하는 쪽(클래스나 @Bean 메서드)에 붙여 "여럿이면 이것을 먼저 쓴다"고 표시한다. @Qualifier는 주입받는 쪽(생성자 파라미터나 필드)에 붙여 쓸 빈의 이름을 직접 적는다. 둘이 함께 있으면 @Qualifier로 지정한 빈이 들어간다.
- NoSuchBeanDefinitionException과 NoUniqueBeanDefinitionException
- 컨테이너가 주입할 빈을 정하지 못했을 때 던지는 두 예외다. NoSuchBeanDefinitionException은 필요한 타입의 빈이 하나도 등록돼 있지 않을 때, NoUniqueBeanDefinitionException은 같은 타입의 빈이 둘 이상이라 하나를 고를 수 없을 때 발생한다. 싱글톤 빈은 애플리케이션이 뜰 때 만들어지므로, 이 예외들은 싱글톤 빈이라면 대개 요청을 처리하는 도중이 아니라 기동 시점에 나타나 애플리케이션이 뜨지 않는다. 앞의 것은 빈 등록(@Component나 @Bean)이 빠졌는지, 뒤의 것은 같은 타입의 빈이 둘 이상 등록되지 않았는지 확인한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 두 구현을 모두 @Component로 등록했습니다. 컨테이너가 고르지 못해 기동이 실패합니다.
- 직접 new로 만들면 — 어떤 구현을 쓸지가 OrderService 안에 적힙니다. 바꾸려면 OrderService를 고칩니다.
- 구현 하나만 등록 — 컨테이너가 하나뿐인 빈을 찾아 넣어 줍니다. OrderService에는 new가 없습니다.
- @Primary로 고르기 — 둘 다 등록하고 먼저 쓸 쪽에 표시를 붙입니다.
- @Qualifier로 고르기 — 주입받는 쪽에서 빈 이름을 지정합니다.
- @Bean 메서드로 등록 — 설정 클래스의 메서드가 돌려준 객체가 빈이 됩니다.
- 빈을 등록하지 않으면 — 넣어 줄 빈이 없어 기동이 실패합니다.
처음 설정으로 실행하면
- 컨테이너 시작: 컨테이너가 클래스들을 훑어 @Component가 붙은 것을 빈으로 등록합니다(컴포넌트 스캔). 등록된 빈은 orderService, fixedDiscountPolicy, rateDiscountPolicy입니다. 빈의 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 것입니다.
- orderService 빈 만들기: OrderService의 생성자가 DiscountPolicy 타입의 인자를 요구합니다. 컨테이너가 등록된 빈 가운데 이 타입에 맞는 것을 찾습니다. 후보는 2개입니다(fixedDiscountPolicy, rateDiscountPolicy).
- 후보가 둘인데 어느 쪽을 넣을지 정할 근거가 없습니다. 컨테이너는 임의로 하나를 고르지 않고 NoUniqueBeanDefinitionException을 던집니다. 애플리케이션 기동이 실패합니다.
실행 결과
- 컨테이너는 타입으로 빈을 찾습니다. 같은 타입의 빈이 둘 이상이면 무엇을 넣을지 알려 줘야 합니다. 먼저 쓸 빈에 @Primary를 붙이거나, 주입받는 곳에 @Qualifier로 빈 이름을 적습니다.
- 구현 클래스를 하나 더 만들어 등록하기만 해도 이 오류가 납니다. 기존 코드를 고치지 않았는데 기동이 실패했다면 같은 타입의 빈이 늘지 않았는지 확인합니다.
코드
interface DiscountPolicy {
int discount(int price);
}
@Component
class FixedDiscountPolicy implements DiscountPolicy {
public int discount(int price) { return 1000; } // 언제나 1000원
}
@Component
class RateDiscountPolicy implements DiscountPolicy {
public int discount(int price) { return price / 10; } // 10%
}
@Component
class OrderService {
private final DiscountPolicy discountPolicy;
OrderService(DiscountPolicy discountPolicy) { // 컨테이너가 넣어 준다
this.discountPolicy = discountPolicy;
}
int order(int price) {
return price - discountPolicy.discount(price);
}
}
// 쓰는 곳: 컨테이너에서 꺼낸다
OrderService orderService = context.getBean(OrderService.class);
int pay = orderService.order(20000);
이 모형의 전제
서른 가지 조합을 이 PC의 스프링 부트 4.1.1(스프링 프레임워크 7.0.9)에서 실행해 대조했습니다. 조합마다 화면과 같은 구조의 소스를 컴파일해, 직접 new로 만드는 경우는 그대로 실행하고 컨테이너를 쓰는 경우는 컴포넌트 스캔으로 띄운 뒤, 기동 성공 여부와 예외 이름, OrderService에 들어간 구현, 할인 금액, 구현 객체의 생성자를 부른 쪽(호출 스택으로 확인), 만들어진 구현 객체의 수를 비교했습니다. "구현을 바꿀 때 고치는 파일"은 쓰려는 구현만 바꾼 두 소스에서 내용이 달라진 파일을 대조 프로그램이 직접 비교한 것입니다. 기동 실패에서 맨 안쪽 원인 예외의 이름을 적었으며, 바깥에는 UnsatisfiedDependencyException이 감싸고 있습니다. 화면에서 단순화한 것: 컨테이너가 빈 객체를 만드는 순서는 등록 순서에 따라 달라지므로 단정하지 않았습니다(순서는 "빈 생성과 주입" 실습에서 다룹니다). 두 구현을 모두 등록하고 고르는 표시가 없어도 생성자 파라미터의 이름이 빈 이름과 같으면(예: rateDiscountPolicy) 그 빈이 들어가 기동에 성공하는 것도 실행으로 확인했습니다. 이 실습의 파라미터 이름은 discountPolicy라서 어느 빈 이름과도 같지 않습니다. 필드 주입과 세터 주입, 같은 타입의 빈을 List로 모두 받는 방법은 다루지 않았습니다.
다음 실습: 빈 생성과 주입