목차
개요
스프링 순환 참조(Circular Reference)란 서로 다른 빈들이 서로 참조를 맞물리게 주입되면서 생기는 현상입니다. beanA에서 beanB를 참조하게되는데 beanB에서도 beanA를 참조해야 하는 경우 순환참조 문제가 생기게 된다.
애플리케이션 로딩 환겨에서 순환참조 문제가 발생하는 상황
출처: https://ch4njun.tistory.com/269
즉, 스프링에서 어떤 스프링 빈을 먼저 만들어야 할 지 결정할 수 없게되는 상황이라 할 수 있다.
순환참조가 발생하는 경우 3가지
순환참조는 맞물리는 DI(Dependency Injection)상황에서 스프링이 어느 스프링 빈을 먼저 생성할지 결정하지 못하기 때문이다. 그렇기 때문에 이 순환참조 문제도 DI를 하는 방법 3가지 상황에서 발생할 수 있는데, 생성자 주입방식, 필드 주입방식, Setter주입 방식이 있고, 이 중에서 생성자 주입 방식은 순환참조문제가 다르게 발생한다. 각각 하나씩 알아보자.
생성자 주입 방식
@Component
public class BeanA {
private BeanB beanB;
public void BeanA(BeanB beanB){
this.beanB = beanB;
}
}
@Component
public class BeanB {
private BeanA beanA;
public void BeanB(BeanA beanA){
this.beanA = beanA;
}
}
Java
복사
애플리케이션을 구동하면 이제 스프링 컨테이너(IOC)는 BeanA 빈을 생성하기위해 BeanB를 주입해줘야하기 때문에 BeanB를 찾을 것이다. 근데 BeanB를 생성하려 하니 BeanA가 필요해서 BeanA를 주입하기위해 BeanA를 찾게되면서 무한 반복이 생기게 된다.
필드, Setter 주입 방식
필드와 Setter 주입방식은 순환참조가 발생하는 시점이 생성자 주입방식과는 다르다.
@Component
@Slf4j
public class BeanA {
@Autowired
private BeanB beanB;
public void run(){
beanB.run();
}
public void call(){
log.info("called BeanA");
}
}
@Component
@Slf4j
public class BeanB {
@Autowired
private BeanA beanA;
public void run(){
log.info("Called BeanB");
beanA.call();
}
}
Java
복사
Field Injection 방식
@Component
@Slf4j
public class BeanA {
private BeanB beanB;
@Autowired
public void setBeanB(BeanB beanB){
this.beanB = beanB;
}
public void run(){
beanB.run();
}
public void call(){
log.info("called BeanA");
}
}
@Component
@Slf4j
public class BeanB {
private BeanA beanA;
@Autowired
public void setBeanA(BeanA beanA){
this.beanA = beanA;
}
public void run(){
log.info("Called BeanB");
beanA.call();
}
}
Java
복사
Setter Injection 방식
이 두 가지 DI 방식은 순환참조문제가 애플리케이션 구동 당시에는 발생하지 않는다.
이 두 가지 방식은 애플리케이션 구동 시점에서는 필요한 의존성이 없을 경우에는 null 상태로 유지하고 실제로 사용하는 시점에 주입을 하기 때문이다. 그렇기에 위 두 가지 방식은 모두 순환참조를 일으킬 수 있는 메서드를 호출하는 시점에서 순환참조 문제가 발생할 것이다.
해결책
순환참조가 무엇이고, 어떻게 발생하는지도 알아보았다.
그럼 어떻게 해결해야 할까? 순환을 끊음으로써 순환참조 문제를 해결해야하는데, 스프링에서는 @Lazy라는 애노테이션을 통해 이런 순환참조를 끊을수 있도록 한다.
@Component
public class BeanA {
private BeanB beanB;
public void BeanA(BeanB beanB){
this.beanB = beanB;
}
}
@Component
public class BeanB {
private BeanA beanA;
public void BeanB(@Lazy BeanA beanA){
this.beanA = beanA;
}
}
Java
복사
하지만, @Lazy는 추천하지 않는다.
그런데 정작 스프링에서는 이 방식을 추천하지 않는다. 공식문서에서는 지연 초기화하는 이 방식의 단점으로 애플리케이션에서 문제를 발견하는게 늦어진다는 점을 들었다.
만약, 스프링 빈이 잘못 구성되어 있는데 초기화가 지연된다면, 애플리케이션은 이 문제를 발견하지 못하고 있다가 나중에 빈이 초기화되는 시점에서 발견하게 된다. 늘 말하지만, 모든 문제는 최대한 빠른 시점에 알게되는게 좋다. 런타임 예외보다는 컴파일 예외가 낫다는 점과 동일하다.
문제는 또 있다.
해당 빈이 초기화가 되는 시점에 JVM의 힙 메모리의공간이 충분한지도 불분명하다. 혹시라도 힙 메모리가 부족해서 빈이 생성될 인스턴스가 저장될 메모리 공간이 없다면 이 역시 문제가 된다.
그렇기에 스프링측에서는 지연 초기화를 권장하지 않는다.
그럼 어떻게 하라는걸까?
그럼 순환참조를 끊으라고 알려준 @Lazy 애노테이션을 사용하지 말라고하면 어떻게 해야할까?
우리는 좀 더 근본적으로 이 설계에 대해 고민할 필요가 있다.
즉, 이렇게 순환참조가 되지 않도록 설계를 해야한다는 것이다.