Spring Framework 시리즈 6화 – 의존성 순환 오류 해결 전략
스프링에서 발생할 수 있는 대표적인 구조적 문제, 의존성 순환 오류(Dependency Circular Reference)의 원인과 해결 방법을 예제 중심으로 정리합니다. @Lazy, 세터 주입, 구조 분리 등 실전 대응 전략도 함께 제공합니다.
순환 의존성이란 무엇인가?
의존성 주입이란 객체 간의 관계를 외부에서 설정해주는 것을 말합니다. 그런데 두 객체가 서로를 참조하면, 객체 생성 시점에서 무한 루프와 같은 문제가 발생하게 됩니다.
예를 들어 A → B, B → A가 동시에 일어날 경우, 스프링은 어느 쪽을 먼저 만들어야 할지 결정할 수 없습니다.
예제 – 순환 의존성 발생
@Component
public class A {
private final B b;
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
public B(A a) {
this.a = a;
}
}
이 코드는 org.springframework.beans.factory.UnsatisfiedDependencyException을 발생시킵니다.
왜 발생하는가?
- A를 생성하려면 B가 먼저 필요함
- B를 생성하려면 다시 A가 필요함
- 생성자 주입(순수 생성자 기반)에서는 객체 생성 시점에 완전한 의존성 확보가 요구되기 때문에 순환이 불가능
해결 방법 ① – 세터 주입 또는 필드 주입으로 변경
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
- 생성자 주입은 즉시 주입을 요구하지만
- 세터/필드 주입은 Spring이 일단 빈을 등록한 뒤, 순차적으로 주입하므로 순환 처리가 가능
❗ 단점: 필수 의존성이라는 보장이 깨지므로 설계 측면에서 유연성은 높지만 안정성은 낮아짐
해결 방법 ② – @Lazy를 활용한 지연 주입
@Component
public class A {
private final B b;
public A(@Lazy B b) {
this.b = b;
}
}
- @Lazy는 주입 시점이 아닌 실제 호출 시점에 객체를 생성
- Spring이 순환 관계에 있는 두 빈을 임시로 등록한 뒤, 프록시를 통해 지연 처리
✅ 실무에서는 생성자 주입을 유지하면서 순환을 해소할 수 있는 가장 안정적인 방법
해결 방법 ③ – 구조를 리팩토링하여 의존 분리
가장 권장되는 방법은 두 컴포넌트의 의존 관계 자체를 끊는 것입니다.
예시: 공통 인터페이스 또는 서비스 계층을 분리
public interface Mediator {
void doSomething();
}
@Component
public class MediatorImpl implements Mediator {
private final A a;
private final B b;
public MediatorImpl(A a, B b) {
this.a = a;
this.b = b;
}
public void doSomething() {
// a와 b의 공통 기능 조율
}
}
- 직접 A ↔ B 구조를 제거하고, 중재자(Mediator)를 통해 간접 의존 구조로 바꿈
- 도메인 간 결합도를 낮추고, 유닛 테스트도 쉬워짐
실무 사례 – 순환 의존성 발생 위치
발생 위치 원인
서비스 → 리포지토리 → 서비스 | 동일한 서비스가 여러 컴포넌트에서 호출되며 참조될 때 |
AOP 적용된 클래스 ↔ 트랜잭션 관리 빈 | Proxy Bean이 적용된 상황에서 DI 충돌 |
@Transactional → @Async → 내부 DI 호출 | 프록시 기반 빈이 순환될 때 발생 가능성 높음 |
순환 참조를 강제로 허용하는 설정 (비추천)
Spring Boot 2.6부터는 순환 참조가 기본적으로 금지되어 있습니다.
spring.main.allow-circular-references=true
- 단기 해결에는 도움이 되지만, 결합도를 높이고 설계 결함을 은폐하므로 비추천
- 테스트용 환경이나 급한 배포 이전에만 일시적으로 고려 가능
마무리 – 순환 참조는 구조 설계의 시그널이다
순환 참조는 단순한 코드 오류가 아니라 설계의 방향이 잘못되었음을 알리는 구조적 시그널입니다.
실제 현업에서도 순환 참조를 발견하면 무조건 @Lazy나 세터 주입으로 해결하기보다는 다음과 같은 순서로 접근합니다:
- 진짜 필요한 의존성인지 검토
- 중재 서비스 혹은 인터페이스로 리팩토링 가능성 확인
- 도메인 간 책임 분리 재설계
- 그럼에도 불가피하다면 @Lazy, 세터 주입으로 회피
다음 7화에서는 @Value와 Environment 객체를 활용해 외부 설정값을 주입하는 방법을 실습합니다. 프로퍼티 파일을 사용하는 이유, 스프링 프로파일과의 연계, 보안 정보를 안전하게 주입하는 전략까지 함께 다룹니다.