실전 고수 팁 — 빈 관리와 순환 참조 방어 전략
스프링 프레임워크를 실무에서 사용할 때 자주 마주치는 문제 중 하나가 빈(Bean) 생명주기 제어와 순환 참조(Circular Dependency) 문제입니다. 이 문서는 실무 환경에서 이런 문제를 어떻게 안정적으로 해결하는지 다룹니다.
1. 순환 참조(Circular Dependency)란?
순환 참조는 A 빈이 B 빈을 의존하고, B 빈이 다시 A 빈을 의존하여 스프링 컨테이너가 어떤 빈을 먼저 생성해야 할지 결정하지 못하는 상태입니다. BeanCurrentlyInCreationException이 주로 발생합니다.
나쁜 예제 (순환 참조 발생)
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
@Service
public class PaymentService {
private final OrderService orderService;
public PaymentService(OrderService orderService) {
this.orderService = orderService;
}
}
[!WARNING] Spring Boot 2.6 버전부터는 기본적으로 순환 참조를 허용하지 않으며, 구동 시 즉시 예외를 발생시키고 서버가 종료됩니다.
2. 해결 방법: 아키텍처 재설계
가장 완벽한 해결책은 설계를 변경하여 순환 참조 고리를 끊어내는 것입니다. 보통 한 계층(Facade 패턴 등)을 더 두거나 중간 매개 서비스(Event 등)를 활용합니다.
@Service
public class OrderFacade {
private final OrderService orderService;
private final PaymentService paymentService;
// 두 서비스를 상위에서 주입받아 조율합니다.
}
3. 실전 대안: @Lazy를 이용한 지연 초기화
레거시 코드 등 당장 구조적인 리팩토링이 불가능할 경우 실전에서 가장 많이 쓰는 방법은 @Lazy입니다.
빈 생성 시점에 진짜 객체 대신 프록시 객체를 주입하고, 실제로 해당 빈의 메서드가 호출될 때 빈을 초기화합니다.
@Service
public class PaymentService {
private final OrderService orderService;
// @Lazy를 통해 OrderService 주입을 지연시킵니다.
public PaymentService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
public void processPayment() {
// 이 시점에 진짜 OrderService가 로드됩니다.
orderService.updateOrderStatus();
}
}
[!TIP]
@Lazy는 임시방편일 뿐입니다. 근본적인 원인을 객체 분리를 통해 해결하는 것이 도메인 로직의 복잡성을 낮추는 정석입니다.