순환참조에러 (1)

스프링 순환참조 문제

스프링으로 프로젝트를 진행하다보면 여러 빈들의 순환참조로 인해 앱 기동이 안되는 상황에 종종 맞딱드리게 됩니다. 최근에 젠킨스로 빌드한 앱이 순환참조 문제로 인해 정상적으로 기동이 되지 않는 문제가 발생했습니다. 그런데 웃긴건 로컬환경에서는 아무런 문제없이 잘 돌아간다는 것이었죠. 결국 담당자의 확인하에 젠킨스의 파이프라인 옵션 설정으로 해결은 했습니다만, setter injection (field injection) 이라도 순환참조가 발생하는 디자인은 좋지 않죠. 그래서 관련내용을 알아보다가 끄적여봅니다.

순환참조란? (What is Circular Reference ? )

순환참조란 서로 다른 여러 빈들이 서로 물고늘어져서 계속 연결되어 있음을 의미합니다.

즉, 아래처럼 A는 B에서 필요한데 B는 또 A에서 필요한 상태를 말합니다.

Bean A → Bean B → Bean A

 

만약 Bean A -> Bean B -> Bean C 처럼 연결되어있다면 스프링은 A를 먼저 만들고 A를 필요로 하는 B를 만들고 B를 필요로 하는 C를 만들게 됩니다. 하지만 순환참조가 발생하면 스프링은 어느 빈을 먼저 생성해야할지 결정하지 못하게되고 순환참조 오류가 발생하게 됩니다. 이렇게 순환참조가 발생한다는건 결국 설계가 잘못되었다는 것입니다. 하지만 그렇다고 설계를 다 뜯어고치자니 비용이 너무 많이 들어갈 수도 있죠. 순환참조 오류는 참고로 스프링의 의존성 주입방법 중에서도 특히 생성자 주입방법을 사용했을 때 발생합니다. 빈 생성시 필요한 다른 빈이 서로 물고늘어져 있으니 어떤 빈도 생성이 불가능한 상황이 되어버리는 것이죠.

순환참조 문제 해결방법

순환참조는 발생하지 않도록 해주는게 제일 좋긴 하지만 어쩔 수 없는 상황은 꼭 생기기 마련입니다. 그럼 어떻게 순환참조 문제를 해결할 수 있을까요? 가장 우선시 해야할 것은 순환참조의 고리를 끊어버리는 것입니다. 이것이 스프링에서 권장하는 방법이며 설계를 조금만 바꿔서 해결이 가능한 경우가 이에 해당합니다. 하지만 만약 설계의 변경이 힘든 경우라면 @Lazy 어노테이션을 사용 해볼 수 있습니다. (해볼 수 있다 라고 얘기한 이유는 아래에 나옵니다)

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

 

이렇게 되어있던 코드를 아래처럼 바꿔주는 것이죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(@Lazy BeanB beanB) {
        this.beanB = beanB;
    }
}

 

하지만 이런 접근 방식은 문제가 있습니다. Lazy initialization 에 대한 문제는 Spring 공식문서(Lazy Initialization)를 참고하시면 되겠습니다. 간단히 말씀드리면 앱 기동시점이 아닌 실제 해당 빈이 필요한 시점에 빈을 생성하기 때문에 특정 http 요청을 받았을 때 힙메모리가 증가할 수 있으며 메모리가 충분하지 않은 상황이었다면 장애로 이어질 수 있다는 얘기죠. 아무튼 스프링에서 권장하지 않는 방식이니 사용하지 않으시면 됩니다. 

 

또 다른 방법은 생성자 주입방법 대신에 setter 주입방법을 사용하시면 됩니다.

아래 setter 주입 예제를 한번 보시죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }

    public BeanB getBeanB() {
        return beanB;
    }
}

@Component
public class BeanB {

    private BeanA beanA;

    @Autowired
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

 

 

혹시나 주입방법에 다시 한번 짚고 넘어가고 싶으시다면 스프링 의존성 주입 포스팅을 참고하시기 바랍니다.

 

Conclusion

스프링으로 개발을 하다가 순환참조가 발생한다면 가장 좋은 해결책은 순환참조의 연결고리를 끊어버리는 것이며 그렇게 하는 것이 깔끔하게 설계된 디자인이라고 할 수 있습니다. 스프링에서도 권장하는 방식이죠. 그래서 setter주입이 아닌 생성자 주입방식을 사용하는 것이 권장됩니다. 우회적으로 순환참조를 잠시 피한다 하더라도 언제 또 똑같은 문제가 발생할지 아무도 알 수 없으며 메모리 부족(lazy initialization의 경우) 현상이 발생할 수도 있기 때문에 가급적 순환참조해야하는 설계는 피하도록 하는 것이 좋겠습니다.

 

 

 

 

Reference

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

- docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies