순환 참조 문제(Setter 주입 vs 생성자 주입)

2022. 11. 2. 19:46Spring 기초

순환참조란

A 클래스가 B 클래스의 Bean을 주입받고, B클래스가 A 클래스의 Bean을 주입받는 상황처럼 서로 순환되어 참조되는 경우를 말한다.

특정 클래스에서 DI를 받을 수 있는 방법은 필드 주입, Setter 주입, 생성자 주입이 대표적으로 가능하다.

이번에는 지난번 Setter 주입과 생성자 주입에서 각각 순환참조 문제가 다르게 나타나는 것을 살펴본다.

준비한 예제에서는 DependencyA와 DependencyB가 서로 순환 참조를 하고 있는 상황이다.

Setter 주입의 경우

class DependencyA {
    private DependencyB dependencyB;

    @Autowired
    void setDependencyA(DependencyB dependencyB) {
        this.dependencyB = dependencyB;
    }

    void run() {
        dependencyB.run();
        System.out.println("A run called!!");
    }
}

class DependencyB {
    private DependencyA dependencyA;

    @Autowired
    void setDependencyB(DependencyA dependencyA) {
        this.dependencyA = dependencyA;
    }

    void run() {
        dependencyA.run();
        System.out.println("B run called!!");
    }
}

@Configuration
class CircularConfig{
    @Bean
    public DependencyA dependencyA() {
        return new DependencyA();
    }

    @Bean
    public DependencyB dependencyB() {
        return new DependencyB();
    }
}

public class CircularDependency {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(CircularConfig.class);
        DependencyA beanA = ac.getBean(DependencyA.class);
        DependencyB beanB = ac.getBean(DependencyB.class);
        
//        beanA.run();
//        beanB.run();
    }
}

결론부터 말하자면 Setter 주입은 순환 참조가 일어나도 어플리케이션이 잘 구동된다.
문제가 발생하는 순간은 현재 주석으로 처리한 메소드를 실행하는 경우다.
순환 참조 상황에서 해당 메소드를 호출하는 경우 런타임시에 에러가 발생하는 것이다.

그런데 이런 경우는 클래스 간 순환참조보다는 각각의 메소드가 서로 호출하는 순환 호출 문제에 가깝다고 생각한다.
중요한 것은 Setter 주입은 순환참조가 일어나도 실행 전까지는 알 수 없다는 점이다.

2. 생성자 주입

스프링은 기본적으로 빈을 생성할 때,
A 클래스가 B 클래스를 의존하고 B 클래스가 C 클래스를 의존한다면
A 클래스에 대한 Bean 을 만들기 위해서 B 클래스의 Bean 을 주입한다. 하지만 B 클래스의 Bean은 없는 상황이다.
따라서 B 클래스의 Bean 을 먼저 만든다. 근데 그 과정에서 또 C 클래스의 Bean 을 주입하는데 없으니까 C 클래스의 Bean 을 먼저 만들게 된다.

생성 순서는 C → B → A가 되겠다.

@Component
class DependencyA {
    private final DependencyB dependencyB;

    public DependencyA(@Lazy DependencyB dependencyB) {
        this.dependencyB = dependencyB;
    }
}
@Component
class DependencyB {
    private final DependencyA dependencyA;

    public DependencyB(DependencyA dependencyA) {
        this.dependencyA = dependencyA;
    }
}

@Configuration
@ComponentScan
class CircularConfig{
}

public class CircularDependency {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(CircularConfig.class);
        DependencyA beanA = ac.getBean(DependencyA.class);
        DependencyB beanB = ac.getBean(DependencyB.class);

    }
}

그런데 우리의 상황에서는 DependencyA → DependencyB → DependencyA → DependencyB → ..
끝없이 순환하다가 결국 아무런 Bean도 생성하지 못하는 에러가 발생한다.

 

해결방안

  1. Redesign
    순환 참조가 발생하지 않도록 설계하는 것이 당연히 베스트다. 순환 고리를 끊어버려라

  2. @Lazy어노테이션을 사용하는 방법인데 한쪽을 Lazy initialization으로 설정해서 어플리케이션 기동 시점이 아닌 빈이 필요한 시점에 빈을 생성하는 방식이다.다만 스프링에서 권장하지 않는 방식이다. 만약 사용하게 된다면 아래 레퍼런스에서 장단점을 잘 확인하자.
    ⇒ 특정 요청을 받았을 때 Heap 메모리가 증가할 수 있으니 메모리가 딸리는 경우 장애로 이어질 수 있다고 한다.
    https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#boot-features-lazy-initializatio
  3. Setter 주입, 필드 주입을 사용하라
    본 글 처음에 Setter 주입에서의 순환참조를 살펴보았는데 어플리케이션은 잘 구동되나 순환 호출하는 메소드를 호출할 때 에러가 발생했다.
    ⇒ 즉, 임시 방편이라고 볼 수 있겠다.

  4. @PostConstruct
    참고한 사이트에서 예제 코드를 가져왔다.

    순환 참조 객체의 한 쪽에서 @PostConstruct를 이용한다.
@Component
public class CircularDependencyA {

    @Autowired
    private CircularDependencyB circB;

    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}

@Component
public class CircularDependencyB {

    private CircularDependencyA circA;
	
    private String message = "Hi!";

    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
	
    public String getMessage() {
        return message;
    }
}

@PostConstruct는 생성자와 함께 쓰일 수 없다. 빈이 생성되고 의존관계 주입이 이루어진 뒤 초기화 콜백 시점에서 동작하기 때문이다. 대충 이런 방법도 있구나.. 하고 넘어가자

요약하자면 생성자 주입은 어플리케이션 로딩 시점에 순환참조 에러를 알려준다. 완벽한 해결방안은 없으니 설계부터 잘 하자!

참고

https://www.baeldung.com/circular-dependencies-in-spring

 

Circular Dependencies in Spring | Baeldung

A quick writeup on dealing with circular dependencies in Spring: how they occur and several ways to work around them.

www.baeldung.com