CATEGORY

카테고리 (612)
AI (17)
Language & Specs (259)
FrameWork (34)
Library (19)
App (41)
Git (8)
Build & Dependency (2)
AWS (15)
DataBase (45)
OS (33)
Tool (16)
IT (120)
반응형
SEEMINGLY ONLINE

Seemingly
Online

이모저모 방방곡곡 두루두루 개발지식 저장소

RECENT POSTS

FrameWork/Spring

Spring 순환참조 해결 — Circular references 에러 3가지 해법

반응형

Spring Boot 2.6부터 빈 사이의 순환참조는 기본으로 금지돼서, 예전엔 뜨던 앱이 업그레이드 후 The dependencies of some of the beans in the application context form a cycle로 기동에 실패합니다. 해결법은 셋입니다. ① 설계를 갈라 순환을 없애기(정답), ② @Lazy로 프록시를 주입해 고리 끊기, ③ spring.main.allow-circular-references=true로 되돌리기(최후수단). 흔한 오해가 하나 있는데 필드·세터 주입으로 바꾸는 건 Boot 2.6+ 기본 설정에서 해결책이 아닙니다. 아래는 Spring Framework 6.1.14 / JDK 25에서 실제로 돌려 확인한 결과입니다.

1. 순환참조 에러 메시지부터 정확히 읽기

순환참조는 A 빈이 B를 필요로 하고 B가 다시 A를 필요로 하는 상태입니다. 가장 단순한 형태는 이렇습니다.

@Service
public class OrderService {
    private final MemberService memberService;
    public OrderService(MemberService memberService) {   // A → B
        this.memberService = memberService;
    }
}

@Service
public class MemberService {
    private final OrderService orderService;
    public MemberService(OrderService orderService) {    // B → A
        this.orderService = orderService;
    }
}

이 상태로 컨텍스트를 띄우면 Spring Framework 레벨에서는 다음 예외가 납니다. (직접 실행해서 받은 메시지 그대로입니다)

org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'orderService':
Unsatisfied dependency expressed through constructor parameter 0:
Error creating bean with name 'memberService':
Unsatisfied dependency expressed through constructor parameter 0:
Error creating bean with name 'orderService':
Requested bean is currently in creation:
Is there an unresolvable circular reference?

Spring Boot 애플리케이션이면 여기에 실패 분석기(FailureAnalyzer)가 붙어 훨씬 친절한 형태로 나옵니다. 아래 문구는 spring-boot-3.3.5.jarBeanCurrentlyInCreationFailureAnalyzer에서 그대로 확인한 것입니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   orderService
┌─────┐
|  memberService
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by
default. Update your application to remove the dependency cycle between beans.
As a last resort, it may be possible to break the cycle automatically by setting
spring.main.allow-circular-references to true.

"prohibited by default"가 핵심입니다. 이 동작은 Spring Boot 2.6에서 바뀌었습니다. 릴리스 노트 원문은 이렇습니다.

“Circular references between beans are now prohibited by default. If your application fails to start due to a BeanCurrentlyInCreationException you are strongly encouraged to update your configuration to break the dependency cycle.”

2. 필드 주입으로 바꾸면 된다? — Boot 2.6+ 에서는 아닙니다

검색하면 "생성자 주입 대신 @Autowired 필드 주입이나 세터 주입을 쓰면 해결된다"는 답이 많이 나옵니다. 절반만 맞습니다. 순수 Spring Framework의 기본값(allowCircularReferences = true)에서는 통하지만, Spring Boot 2.6 이상은 이 값을 false로 꺼 두기 때문에 필드 주입이어도 똑같이 죽습니다. 세 가지 조합을 실제로 돌려봤습니다.

주입 방식 allowCircularReferences 결과
생성자 주입 true / false 무관 ❌ 기동 실패
필드(세터) 주입 true (순수 Spring · Boot 2.5 이하 기본) ✅ 기동 성공
필드(세터) 주입 false (Boot 2.6+ 기본) ❌ 기동 실패
생성자 주입 + @Lazy false (Boot 2.6+ 기본) ✅ 기동 성공

왜 생성자 주입만 allowCircularReferences와 무관하게 실패할까요? 생성자 주입은 객체를 만드는 그 순간에 상대 빈이 완성돼 있어야 하기 때문입니다. A를 만들려면 B가 필요한데 B를 만들려면 다시 A가 필요하니 빠져나갈 구멍이 없습니다. 반면 필드·세터 주입은 일단 빈을 만들어 놓고 나중에 값을 꽂는 방식이라, Spring이 "아직 덜 만들어진 A"를 임시로 내주면 고리가 풀립니다. 이 임시 참조를 허용할지 결정하는 스위치가 바로 allowCircularReferences이고, Boot 2.6이 이걸 껐습니다.

3. 해결법 ① 설계를 갈라서 순환을 없앤다 (권장)

대부분의 순환참조는 "서비스가 다른 서비스의 메서드 하나를 쓰고 싶어서" 생깁니다. 이때는 그 메서드를 별도 클래스로 빼거나, 서비스 대신 리포지토리를 직접 주입하면 고리가 끊깁니다.

// Before: OrderService ↔ MemberService (서로 참조)
// MemberService 가 필요했던 건 사실 "회원 조회" 하나뿐이었다.

@Service
public class OrderService {
    private final MemberRepository memberRepository;   // 서비스 대신 리포지토리
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

// After: OrderService → MemberRepository  (단방향, 순환 없음)

공통 로직이 양쪽에서 쓰이면 제3의 컴포넌트로 추출합니다. A → C ← B 형태가 되어 순환이 사라집니다. 이벤트 성격이라면 ApplicationEventPublisher로 뒤집는 것도 방법입니다. 순환참조 에러는 사실 "두 클래스의 책임이 섞여 있다"는 설계 신호라, 이 방향이 정답입니다.

4. 해결법 ② @Lazy 로 프록시를 주입해 고리 끊기

당장 구조를 못 바꿀 때는 한쪽 생성자 파라미터에 @Lazy를 붙입니다. 그러면 Spring이 실물 빈 대신 프록시를 먼저 꽂아 주고, 실제 메서드를 호출하는 시점에 진짜 빈을 찾아 위임합니다.

@Service
public class OrderService {
    private final MemberService memberService;
    public OrderService(@Lazy MemberService memberService) {   // 프록시가 주입됨
        this.memberService = memberService;
    }
}

이 조합은 allowCircularReferences = false(Boot 2.6+ 기본) 상태에서도 정상 기동하는 것을 직접 확인했습니다. 한쪽에만 붙이면 됩니다. 다만 이건 순환을 없앤 게 아니라 초기화 시점을 미룬 것이라, 순환 자체가 남아 있으니 임시방편으로 보는 게 맞습니다. 참고로 이렇게 주입된 프록시는 Spring @Async 프록시와 마찬가지로 자기 자신 안에서의 내부 호출에는 관여하지 못한다는 점도 알아두면 좋습니다.

5. 해결법 ③ spring.main.allow-circular-references=true (최후수단)

레거시 프로젝트를 Boot 2.6+로 올리는 중이라 당장 기동부터 시켜야 한다면, 설정 한 줄로 2.5 이전 동작을 되살릴 수 있습니다.

# application.properties
spring.main.allow-circular-references=true
# application.yml
spring:
  main:
    allow-circular-references: true

코드로 켤 수도 있습니다.

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApplication.class);
    app.setAllowCircularReferences(true);
    app.run(args);
}

단, 이건 필드·세터 주입 순환만 살려 줍니다. 생성자 주입끼리 물린 순환은 이 옵션을 켜도 그대로 실패하고, 이때 Boot는 다른 메시지를 냅니다 — Despite circular references being allowed, the dependency cycle between beans could not be broken. 즉 이 문구를 봤다면 설정으로는 못 풀고 코드를 고쳐야 한다는 뜻입니다.

자주 묻는 것 (FAQ)

Q. 어느 빈들이 물렸는지 어떻게 찾나요?
Spring Boot 에러 메시지의 ┌─────┐ 사이클 그림에 빈 이름이 순서대로 찍힙니다. 이 목록이 곧 고리이므로, 그중 가장 책임이 애매한 하나를 골라 잘라내면 됩니다.

Q. 자기 자신을 주입하는 것도 순환참조인가요?
A가 A를 주입하는 자기참조(self-injection)도 같은 고리입니다. 프록시를 거쳐 자기 메서드를 부르려는 목적이면 @Lazy를 붙이거나, AopContext.currentProxy()·클래스 분리를 검토하세요.

Q. @Lazy를 붙였는데도 실패합니다.
프록시를 만들 수 없는 경우가 있습니다. 대상 클래스가 final이거나 기본 생성자가 없는 클래스(CGLIB 프록시 대상)면 실패할 수 있습니다. 이때는 인터페이스를 두거나 3번 방법으로 갑니다.

Q. 그냥 옵션 켜고 쓰면 안 되나요?
기동은 되지만, 초기화 순서에 따라 덜 만들어진 빈이 주입될 수 있고 AOP 프록시가 적용되지 않은 원본 객체가 꽂히는 사고도 납니다. Spring 팀이 기본값을 끈 이유가 이것이라, 임시로만 쓰고 설계를 고치는 편이 낫습니다.

마무리

정리하면 순환참조 에러는 Spring Boot 2.6부터 기본 금지가 됐고, 필드 주입으로 바꾸는 옛날 해법은 이제 기본 설정에서 통하지 않습니다. 순서는 ① 설계 분리 → ② @Lazy → ③ allowCircularReferences로 잡으세요. 에러 메시지에 찍힌 빈 목록이 곧 "책임이 섞인 클래스 목록"이니, 급한 불을 끈 뒤에는 그 목록을 다시 열어 보는 걸 권합니다.


📚 참고 출처 (2026년 7월 21일 확인 · 실행 환경: Spring Framework 6.1.14 · Spring Boot 3.3.5 · JDK 25)
· Spring Boot 2.6 Release Notes — Circular References Prohibited by Default
· Spring Boot Reference — Application Properties (spring.main.allow-circular-references)
· Spring Framework Javadoc — @Lazy

반응형

COMMENTS