ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Framework 시리즈 6화 – 의존성 순환 오류 해결 전략
    기술과 산업/언어 및 프레임워크 2025. 5. 27. 19:05
    728x90

    스프링에서 발생할 수 있는 대표적인 구조적 문제, 의존성 순환 오류(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나 세터 주입으로 해결하기보다는 다음과 같은 순서로 접근합니다:

    1. 진짜 필요한 의존성인지 검토
    2. 중재 서비스 혹은 인터페이스로 리팩토링 가능성 확인
    3. 도메인 간 책임 분리 재설계
    4. 그럼에도 불가피하다면 @Lazy, 세터 주입으로 회피

    다음 7화에서는 @Value와 Environment 객체를 활용해 외부 설정값을 주입하는 방법을 실습합니다. 프로퍼티 파일을 사용하는 이유, 스프링 프로파일과의 연계, 보안 정보를 안전하게 주입하는 전략까지 함께 다룹니다.

    728x90
Designed by Tistory.