본문으로 건너뛰기
Advertisement

실전 고수 팁 — 빈 관리와 순환 참조 방어 전략

스프링 프레임워크를 실무에서 사용할 때 자주 마주치는 문제 중 하나가 빈(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는 임시방편일 뿐입니다. 근본적인 원인을 객체 분리를 통해 해결하는 것이 도메인 로직의 복잡성을 낮추는 정석입니다.

Advertisement