티스토리 뷰
순환참조(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. 디자인을 새로 한다
는 방식을 권장하며 꼭 개발할 때 순환참조를 하고 있지는 않는지 확인하자!
참고
'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
- Caching
- fail-fast
- HashSet
- 인스턴스변수
- nginx
- AutoConfiguration
- 자동구성
- java
- 로드 밸런서
- Load Balancer
- 오블완
- Sticky Session
- 다중화
- Hash
- 고정 세션
- @conditional
- nosql
- spring boot
- Security
- Red-Black Tree
- 정적변수
- HashMap
- fail-safe
- JPA
- Spring
- 추상클래스
- 인터페이스
- syncronized
- 티스토리챌린지
- object
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |