본문으로 건너뛰기
개발 머꼬
개발 노트Spring
hohyeon.dev21

Spring 빈 순환 의존성, 애플리케이션이 시작조차 안 되는 이유

  • #Architecture
  • #Engineering Note
  • #Spring

문제 발생

두 서비스가 서로를 참조하도록 생성자 주입을 걸었더니, 애플리케이션이 시작하는 순간 BeanCurrentlyInCreationException이 발생하며 부팅에 실패했습니다.

@Service
public class ServiceA {
    public ServiceA(ServiceB serviceB) { ... }
}

@Service
public class ServiceB {
    public ServiceB(ServiceA serviceA) { ... } // A와 B가 서로를 필요로 함
}

원인 분석

Spring이 ServiceA를 만들려면 먼저 ServiceB가 필요하고, ServiceB를 만들려면 먼저 ServiceA가 필요합니다. 생성자 주입은 객체가 "완전히 만들어진 상태"로만 주입할 수 있으므로, 이 순환을 풀 방법이 없습니다 — 닭이 먼저냐 달걀이 먼저냐의 상황입니다. Spring은 이를 감지하면 애플리케이션 시작을 막고 명확한 에러를 던집니다.

필드 주입이나 setter 주입이었다면 Spring이 "일단 프록시나 미완성 객체를 넣어두고 나중에 채우는" 방식으로 순환을 우회하는 경우가 있었지만(Spring Boot 2.6부터는 이마저도 기본적으로 금지됩니다), 이는 문제를 해결한 게 아니라 숨긴 것에 가깝습니다 — 결국 두 클래스가 서로의 존재에 강하게 결합되어 있다는 설계 문제가 근본 원인입니다.

해결 방안

순환 의존성은 설정으로 우회하기보다 설계를 다시 보는 신호로 받아들이는 것이 맞습니다.

  1. 공통 로직 추출: A와 B가 서로 필요로 하는 기능이 실제로는 제3의 클래스(ServiceC)로 뽑을 수 있는 공통 로직인 경우가 많습니다. A와 B가 각각 C를 의존하도록 바꾸면 순환이 사라집니다.
  2. 이벤트 기반으로 전환: A가 B의 특정 동작을 트리거만 하면 되는 관계라면, 직접 호출 대신 Spring의 ApplicationEventPublisher로 이벤트를 발행하고 B가 그 이벤트를 구독하는 방식으로 결합을 느슨하게 만들 수 있습니다.
  3. 인터페이스 분리: A가 B의 전체 기능이 아니라 일부만 필요하다면, 그 일부만 담은 인터페이스를 만들고 B가 그 인터페이스를 구현하도록 하면 의존 방향을 명확히 한쪽으로 정리할 수 있는 경우가 있습니다.
  4. 정말로 불가피하다면 @Lazy를 순환의 한쪽에 붙여 프록시로 지연 초기화할 수는 있지만, 이는 임시방편이지 권장되는 해결책이 아닙니다 — 근본적으로 두 빈이 서로를 필요로 하는 설계 자체를 재검토하는 것이 우선입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.