티스토리 뷰

Framework/Spring

[Spring] 순환 참조

DUCKBAE's 2022. 12. 13. 16:04

순환참조(Circular dependencis)는 객체 A가 객체 B를 의존하고 있을 때 객체 B가 객체 A를 의존하고 있을 때 발생한다.

즉, 객체끼리 서로 의존하고 있을 때 발생하는 것이다.

 

예로  A, B, C 객체가 있고 A 객체는 B를, B 객체는 C를 의존하고 있다고 가정해보자.

* A -> B -> C

스프링 컨테이너는 A객체를 생성하려고 보니 B객체를 의존하고 있어 B객체를 생성하려고 하는데 B객체는 C객체를 의존하고 있어서 결국엔 C를 먼저 생성하고, B를 생성하고 A를 생성할 것이다.

* C -> B -> A

하지만, 위 객체들이 서로 의존하고 있다고 하면 스프링은 어느 객체를 먼저 생성해야 하는지 결정하지 못한다!

A 객체를 만들려고 보니까 B 객체를 의존하고 있네 ? 근데 B 객체가 없으니까 B부터 만들어볼까

B 객체를 만들려고 보니까 C 객체를 의존하고 있네 ? 근데 C 객체가 없으니까 C부터 만들어볼까

C 객체를 만들려고 보니까 A 객체를 의존하고 있네 ? 근데 A 객체가 없으니까 A부터 만들어볼까

A 객체를 만들려고 보니까 ..

이런식으로 무한루프에 빠지게 되고 결과적으로 어떠한 객체도 생성하지 못하는 문제를 순환참조의 문제라고 한다.

.

.

객체끼리 서로 의존하고 있을 때 발생한다고 하였는데, 객체 간 의존성을 주입하는 대표적인 방법으로 한번 살펴보자.

생성자 주입 방식

@Component
public class CircularDependencyA {

    @Autowired
    public CircularDependencyA(CircularDependencyB circularDependencyB) {
        this.circularDependencyB = circularDependencyB;
    }
}

@Component
public class CircularDependencyB {

    @Autowired
    public CircularDependencyB(CircularDependencyA circularDependencyA) {
        this.circularDependencyA = circularDependencyA;
    }
}

현재 위 코드는 생성자 주입 방식으로 서로를 의존하고 있는 CircularDependencyA <-> CircularDependencyB 관계이다.

위 코드를 기반으로 스프링 애플리케이션을 띄웠을 때 발생하는 로그는 다음과 같다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  circularDependencyA defined in file [C:\Users\kimseyeong\Desktop\workspace\spring-boot-study\study\build\classes\java\main\com\example\study\circular_dependency\CircularDependencyA.class]
↑     ↓
|  circularDependencyB defined in file [C:\Users\kimseyeong\Desktop\workspace\spring-boot-study\study\build\classes\java\main\com\example\study\circular_dependency\CircularDependencyB.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException at DefaultSingletonBeanRegistry.java:355

 

필드 주입 & 수정자 주입

필드 주입 및 수정자 주입의 순환 참조는 컴파일 타임에 알 수 없다고 한다.

즉, 스프링 애플리케이션은 정상적으로 구동되고 순환참조를 하고 있는 코드가 실행이 되어야 (런타임 시점) 순환참조를 하고 있다는 사실을 알게 된다고 한다.

컴파일 시점에 알려주는 생성자 주입보다 더 무서운 것 같다. 매도 빨리 맞는 게 낫다고 !

.

하지만 스프링 부트 2.6 부터 순환 참조를 기본적으로 금지한다고 한다.

Spring Boot 2.6 Release Notes

Circular References Prohibited by Default

나는 스프링 부트 2.7 버전을 사용하고 있는데 필드 주입 및 수정자 주입으로 테스트 해보았을 때, 생성자 주입처럼 컴파일 시점에 순환참조를 하고 있다는 것을 알 수 있었고 이에 스프링 애플리케이션이 구동되지 않았다.

 

하지만 이를 사용해야한다면 !

(이 경우가 뭐가 있을 지 모르겠지만.. 있나 ? 있나요 ?? 전 아직까지 경험해 보지 않았어요)

application.yml 또는 application.properties 파일에 spring.main.allow-circular-references : true 로 설정하면 된다.

그렇게 되면 컴파일 시점에 순환 참조를 체킹하지 않는데 !

문제는 런타임 시점에 순환참조 하고 있는 코드를 실행하면 이제 어떤 코드냐에 따라 다르겠지만 나 같은 케이스는 에러가 발생하는 것이다.

케이스를 한번 확인해보자.

@Component
public class CircularDependencyB {

    private CircularDependencyA circularDependencyA;

    @Autowired
    public void setCircularDependencyA(CircularDependencyA circularDependencyA) {
        this.circularDependencyA = circularDependencyA;
    }

    public void callCircularDependencyA() {
        log.info("Called callCircularDependencyA Method !");
        circularDependencyA.callCircularDependencyB();
    }
}

@Component
public class CircularDependencyA {

    private CircularDependencyB circularDependencyB;

    @Autowired
    public void setCircularDependencyB(CircularDependencyB circularDependencyB) {
        this.circularDependencyB = circularDependencyB;
    }

    public void callCircularDependencyB() {
        log.info("Called callCircularDependencyB Method !");
        circularDependencyB.callCircularDependencyA();
    }
}

//로그 및 에러
Called callCircularDependencyB Method !
Called callCircularDependencyA Method !
Called callCircularDependencyB Method !
Called callCircularDependencyA Method !
..
java.lang.StackOverflowError at CharsetEncoder.java:340

로그를 살펴보면 Called ~ 에러가 무한개로 생성되면서 결국 StackOverflowError 가 발생한다.


순환참조를 해결하기 위한 방법은 무엇이 있을까?

1. 디자인을 새로 한다.

순환 참조가 발생한다는 것은 설계에 문제가 있다는 것이다. *가장 근본 적인 원인 !

따라서 *계층 구조가 잘 설계 되어 있어야 하고 순환 참조가 발생하지 않도록 구성 요소를 잘 설계해야 한다.

하지만 재 설계하는 데 시간과 리소스가 부족하다면 다른 방법을 사용하는 것이 좋다.

 

2. @Lazy 사용하기

순환참조가 발생하는 빈 중에서 빈 하나의 초기화를 늦게하는 것이다.

하지만 이 방법은 스프링에서 권장하지 않는 방법이라고 한다.

이 방법은 객체를 필요할 때 생성하기 때문에 애플리케이션의 문제를 발견하는게 늦어진다는 단점이 있다.

만약 스프링 빈이 잘못 구성되어 있는데 초기화가 지연된다면, 애플리케이션 구동할 때는 문제가 없다가 해당 빈을 사용할 때 알아차리게 된다.

 

또한 해당 빈이 초기화가 되는 시점에 JVM의 힙 메모리공간이 충분한지도 불분명하다.

혹시라도 힙 메모리가 부족해서 빈이 생성될 공간이 없다면 이 역시 문제가 된다.

이런 이유 때문에 지연 초기화는 기본으로 설정되어 있지 않으며, 활성화 하기 전에 JVM의 힙 크기를 조정하는 것이 좋다.

 

그 외에 다양한 방법으로 순환 참조를 막을 수 있지만, 이런 구조는 서로가 서로의 객체를 참조하고 있다는 것이다.

만약 해당 객체들이 서로의 메서드를 호출하게 되어있다면 그 또한 무한루프를 일으키며 결국 메모리는 폭발하게 된다.

따라서 1. 디자인을 새로 한다는 방식을 권장하며 꼭 개발할 때 순환참조를 하고 있지는 않는지 확인하자!

 

참고

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.spring-application.lazy-initialization

'Framework > Spring' 카테고리의 다른 글

[Web] Servlet 과 Servlet Container  (0) 2022.12.26
[Spring] DispatcherServelt (Feat. Front Controller)  (0) 2022.12.16
[JPA] N + 1 문제  (0) 2022.12.05
[JPA] 즉시로딩과 지연로딩  (0) 2022.11.14
[JPA] 프록시 객체  (0) 2022.10.31
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함