이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Spring › IoC 컨테이너와 DI
빈 생성과 주입
스프링은 객체를 어떤 순서로 만들고 연결하나
애플리케이션이 뜰 때 스프링 컨테이너가 빈을 하나씩 만들고 서로 연결합니다. 주입 방식에 따라 객체가 만들어지는 순서가 달라지고, 두 빈이 서로를 필요로 하면(순환 참조) 기동이 실패하기도 합니다.
실행으로 대조 · 스프링 부트 4.1.1로 열두 가지 조합을 띄워 대조
이 실습에 나오는 개념
- IoC(제어의 역전)
- 객체의 생성과 객체 사이의 연결을 개발자의 코드가 아니라 프레임워크(컨테이너)가 맡는 설계 원칙이다. 일반적인 프로그램에서는 내 코드가 필요한 객체를 직접 만들고 호출하지만, IoC에서는 컨테이너가 객체를 만들어 필요한 곳에 넣어 주고 적절한 시점에 내 코드를 호출한다. 제어의 주체가 뒤바뀌었다는 뜻에서 붙은 이름이다.
- DI(의존성 주입)
- 객체가 사용하는 다른 객체(의존성)를 스스로 만들지 않고 외부에서 전달받는 방식이다. 스프링에서는 컨테이너가 빈을 생성하면서 생성자, 세터, 필드를 통해 의존성을 넣어 준다. 구현 클래스를 바꾸거나 테스트에서 가짜 객체를 넣기 쉬워지고 클래스 사이의 결합도가 낮아진다.
- 생성자 주입, 세터 주입, 필드 주입
- 의존성을 넣는 세 가지 방법이다. 생성자 주입은 객체 생성 시 의존성을 받으므로 필드를 final로 둘 수 있고 의존성이 빠진 채 객체가 만들어지는 일이 없어 스프링이 권장한다. 세터 주입은 선택적인 의존성에 쓴다. 필드 주입은 필드에 @Autowired를 붙이는 방식으로 코드는 짧지만 스프링 없이 객체를 만들어 테스트하기 어렵다.
- 스프링 빈(Bean)
- 스프링 IoC 컨테이너가 생성하고 관리하는 객체다. 개발자가 new로 직접 만든 객체와 달리 컨테이너가 의존성을 주입하고 초기화와 소멸을 관리하며, 프록시를 통해 트랜잭션이나 AOP 같은 부가 기능을 적용할 수 있다.
- 빈 생명주기
- 빈이 만들어져 사라지기까지의 과정이다. 객체 생성, 의존성 주입, 초기화 콜백 호출, 사용, 컨테이너 종료 시 소멸 콜백 호출의 순서로 진행된다. 의존성이 모두 주입된 뒤에 해야 하는 초기화 작업은 @PostConstruct에, 종료 전 정리 작업은 @PreDestroy에 둔다.
- 순환 참조
- 빈 A가 B를 필요로 하고 B가 다시 A를 필요로 하는 상태다. 생성자 주입에서는 어느 쪽도 먼저 만들 수 없어 애플리케이션이 시작되지 못한다. Spring Boot 2.6부터는 주입 방식과 관계없이 기본적으로 금지된다. 두 빈의 공통 책임을 별도의 빈으로 분리하는 등 설계를 바꿔 해결하는 것이 원칙이다.
이렇게 바꿔 보세요
- 처음 설정으로 — 생성자 주입. 의존 대상이 먼저 완성돼야 다음 객체를 만들 수 있습니다.
- 필드 주입으로 바꾸면 — 객체를 먼저 만들고 나중에 채웁니다. 잠깐 동안 필드가 비어 있습니다.
- 생성자 주입 + 순환 참조 — 어느 쪽도 먼저 만들 수 없어 기동이 실패합니다.
- 필드 주입 + 순환 참조 — 스프링 부트 기본 설정에서는 이것도 기동 실패입니다.
- 순환 참조를 허용하면 — 필드 주입은 덜 만들어진 객체를 미리 넘겨서 통과합니다.
- 허용해도 생성자 주입은 실패 — 생성자를 호출하려면 인자가 먼저 있어야 하기 때문입니다.
처음 설정으로 실행하면
- 컴포넌트 스캔: @RestController, @Service, @Repository가 붙은 클래스를 찾아 빈 정의 3개를 등록했습니다. 아직 객체는 하나도 만들지 않았습니다.
- OrderController를 만들기 시작합니다. 생성자가 OrderService를 요구하므로, 그것부터 준비해야 생성자를 호출할 수 있습니다.
- OrderService를 만들기 시작합니다. 생성자가 OrderRepository를 요구하므로, 그것부터 준비해야 생성자를 호출할 수 있습니다.
- OrderRepository를 만들기 시작합니다. 생성자에 인자가 없습니다.
- OrderRepository의 생성자를 호출해 객체를 만들었습니다. 만들어지는 순간 의존성이 모두 채워져 있습니다. 이어서 초기화 콜백(@PostConstruct)이 있으면 호출되고, 빈이 완성됩니다.
- OrderService의 생성자를 호출해 객체를 만들었습니다. 인자로 OrderRepository를 넘겼습니다. 만들어지는 순간 의존성이 모두 채워져 있습니다. 이어서 초기화 콜백(@PostConstruct)이 있으면 호출되고, 빈이 완성됩니다.
- OrderController의 생성자를 호출해 객체를 만들었습니다. 인자로 OrderService를 넘겼습니다. 만들어지는 순간 의존성이 모두 채워져 있습니다. 이어서 초기화 콜백(@PostConstruct)이 있으면 호출되고, 빈이 완성됩니다.
실행 결과
- 생성자 주입에서는 의존 대상이 완성된 뒤에야 객체를 만들 수 있습니다. 그래서 객체가 존재하는 순간부터 의존성이 빠짐없이 채워져 있고, 필드를 final로 둘 수 있습니다.
코드
@RestController
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
@Service
class OrderService {
private final OrderRepository orderRepository;
OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
@Repository
class OrderRepository {
}
이 모형의 전제
빈 네 개만 있는 작은 애플리케이션이며, 컨테이너는 스캔한 순서대로 빈을 만들기 시작한다고 가정했습니다(실제 순서는 클래스 경로와 설정에 따라 달라질 수 있지만, 의존 대상이 먼저 준비돼야 한다는 규칙은 같습니다). 순환 참조를 기본으로 금지하는 것은 스프링 부트 2.6 이후의 동작입니다. 순환 참조를 푸는 다른 방법(@Lazy로 주입 시점 늦추기)은 다루지 않았습니다. 열두 가지 조합 모두를 실제 스프링 부트 4.1.1로 띄워, 기동 성공 여부와 객체가 만들어진 순서(생성자가 불린 순서), 완성된 순서(주입이 끝난 순서)가 화면과 일치하는 것을 확인했습니다. 기동 실패는 모두 BeanCurrentlyInCreationException이었습니다.
앞 실습: 스프링 컨테이너와 빈 · 다음 실습: 싱글톤 빈과 상태