[Spring Boot] 순환참조문제
순환참조문제란?
위 예외는 애플리케이션 로딩 과정에서 순환참조가 발생함을 알리는 예외이다.
순환참조 문제란 A 클래스가 B 클래스의 Bean 을 주입받고, B 클래스가 A 클래스의 Bean 을 주입받는 상황처럼 서로 순환되어 참조할 경우 발생하는 문제를 의미한다.
그럼 이러한 순환참조 문제는 어떠한 상황에서 발생하는지 살펴보자.
왜 발생할까?
특정 클래스에서 IoC 컨테이너에 있는 Bean 을 주입받기 위해서 세 가지 방법을 사용할 수 있다. 필드 주입방식, Setter 주입방식, 생성자 주입방식이 이에 해당한다.
갑자기 의존성 주입 방식에 대해서 이야기하는 이유는 생성자 주입방식과 나머지 두 가지 주입방식에서의 순환참조문제가 조금 다르게 발생하기 때문이다.
먼저, 필드 주입방식과 Setter 주입방식에서 발생하는 순환참조문제에 대해서 이야기해보자.
필드 주입방식과 Setter 주입방식에서는 A 클래스가 B 클래스를 의존하고, B 클래스가 A 클래스를 의존하는 상황이더라도 애플리케이션 실행과정에서 예외가 발생하지 않는다.
그리고 실제로 두 개의 클래스가 순환참조하고 있다고 하더라도 당장에 문제가 발생하지 않는다. 이러한 상황에서 문제가 되는 순간은 A 클래스의 메소드와 B 클래스의 메소드가 서로 순환참조하고 있는 상황에서 해당 메소드가 호출되었을 때이다.
@Slf4j
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void run() {
serviceB.run();
log.info("Called ServiceA.run()");
}
}
@Slf4j
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
public void run() {
serviceA.run();
log.info("Called ServiceB.run()");
}
}
정리해보면 기억해야 할 것은 두 가지이다.
- 스프링 애플리케이션 로딩시 예외가 발생하지 않는다.
- 단순히 클래스끼리 순환참조하는 것이 아니라 실제로 메소드가 순환호출되어야 하고, 해당 메소드가 호출되는 시점에 예외가 발생한다.
근데 여기까지 읽었을때 이게 순환참조문제인가..? 라고 생각이 드는게 정상이다!!
이건 순환참조 문제가 아니라 서로다른 메소드가 서로호출할 때 발생하는 순환호출문제이다..!
이제 이어서 생성자 주입방식에서의 순환참조 문제에 대해서 이야기해보자.
우선 생성자 주입방식의 동작방식에 대해서 이야기해보면,
스프링 애플리케이션이 로딩되는 시점에 A 클래스가 B 클래스를 의존하고 B 클래스가 C 클래스를 의존한다면 Spring Boot 는 A 클래스에 대한 Bean 을 만들기 위해서 B 클래스의 Bean 을 주입하는데 없으니까, B 클래스의 Bean 을 먼저 만든다. 근데 그 과정에서 또 C 클래스의 Bean 을 주입하는데 없으니까 C 클래스의 Bean 을 먼저 만든다.
결과적으로 Spring Boot 는 C - B - A 순서로 Bean 을 생성하는 것이다.
그렇다면 A 클래스가 B 클래스를 의존하고, B 클래스가 A 클래스를 의존하는 상황에 대해서 이야기해보자.
A 클래스의 Bean 을 만드는 과정에서 B 클래스의 Bean 을 주입하고, 없으니까 B 클래스의 Bean 을 먼저 생성한다. 이 때 A 클래스의 Bean 을 주입하려는데 없으니까 A 클래스의 Bean 을 먼저 생성한다. 이 때 B 클래스의 Bean 을 주입하려는데 없으니까 A 클래스의 Bean 을 먼저 생성.... 하면서 무한 반복에 빠지게된다.
이렇게 순환되는 과정에서 결과적으로 어떠한 Bean 도 생성하지 못하는 문제를 바로 순환참조문제라고 한다.
정리해보면 기억해야 할 것이 마찬가지로 두 가지이다.
- 클래스가 서로 의존성 주입을 통해 순환참조하고 있을 때 발생하는 문제이다. (메소드까지는 가지도못함..!)
- 스프링 애플리케이션 로딩시점에서 예외가 발생한다!
어떻게 해결할까?
설계상 이렇게 순환참조문제가 발생할 수 있는 구조자체를 만들지 않는 것이 가장 좋다고 한다. 따라서 순환참조가 발생하고 있는 순환의 고리를 끊는 것이 가장 좋은 해결책이다.
하지만 이렇게 순환의 고리를 끊을 수 없는 경우에는 @Lazy 어노테이션을 통해서 임의로 해결할 수 있다.
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(@Lazy ServiceA serviceA) {
this.serviceA = serviceA;
}
}
하지만 이러한 방식은 스프링에서 권장하지 않는 방법이다.
애플리케이션 로딩시점이 아니라 해당 Bean 이 필요한 시점에 주입을 받기 때문에 특정 HTTP 요청을 받았을 때 Heap 메모리가 증가할 수 있으며 메모리가 충분하지 않을 경우 장애가 발생할 수 있다는 이유 때문이다. (흠...)
또 다른 해결 방법은 필드 주입방식 혹은 Setter 를 이용한 주입방식을 이용하는 것이다.
하지만, 이러한 방법도 임시방편일 뿐 앞서 말했듯이 순환참조되는 설계를 지양하는 것이 좋다. 생성자 주입방식의 장점중의 하나도 이러한 순환참조 여부를 애플리케이션 로딩 시점에서 알 수 있다는 점에서 비추어 봤을 때 아주 불가피한 상황이 아니라면 설계를 개선하는 것이 좋을 것같다.
마지막으로 의존성 주입방법에 따른 장단점에 대한 참고자료를 확인해보자.
출처 : https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/