순환참조
- 객체나 클래스가 서로를 참조하는 상황이 계속 순환되는 구조를 말한다.
- 예를 들어, 클래스 A 가 클래스 B 를 참조하고, 클래스 B 도 다시 클래스 A 를 참조하게 되면, 두 클래스가 서로를 필요로 하게 되어 순환 참조가 발생한다.
예를 들어보면,
1. 클래스 간 순환 참조
class A {
private B b;
public A(B b) {
this.b = b;
}
}
class B {
private A a;
public B(A a) {
this.a = a;
}
}
여기서 A 와 B 는 서로를 생성자에서 참조하므로, A 를 생성하려면 B 가 먼저 필요하고, B 를 생성하려면 A 가 필요하다. 이 때문에 객체를 생성하는 과정에서 무한 루프가 생기거나 프로그램이 시작되지 않는 문제가 발생할 수 있다.
2. Enum 클래스 간 순환 참조
public enum EnumA {
TYPE_ONE(EnumB.VALUE_ONE),
TYPE_TWO(EnumB.VALUE_TWO);
private final EnumB relatedEnum;
EnumA(EnumB relatedEnum) { this.relatedEnum = relatedEnum; }
}
public enum EnumB {
VALUE_ONE(EnumA.TYPE_ONE),
VALUE_TWO(EnumA.TYPE_TWO);
private final EnumA relatedEnum;
EnumB(EnumA relatedEnum) { this.relatedEnum = relatedEnum; }
}
두 Enum이 서로를 인수로 사용하려 할 때도 비슷한 상황이 발생할 수 있다.
예를 들어, EnumA 가 EnumB 를 사용하고, 동시에 EnumB 가 EnumA 를 사용하면 순환참조가 발생한다.
Java에서 Enum은 상수가 초기화되는 과정에서 순차적으로 로드되어야 하는데, 서로 참조하게 되면 어느쪽도 완전히 초기화되지 못해 런타임 오류를 일으킨다.
왜 문제가 발생할까?
순환참조는 프로그램이 무한히 대기하거나 예기치 않은 오류를 발생하게 만든다. 메모리 누수를 일으킬 수도 있고, Java에서는 클래스가 로드되지 못하는 상황을 만들기도 한다. 특히 Spring과 같은 프레임워크에서는 순환참조가 애플리케이션 실행을 방해할 수도 있다.
순환참조를 피하는 방법
@Lazy
@Lazy 는 필요한 시점까지 객체 생성을 지연시킴으로써 순환참조 문제를 해결할 수 있다. Spring에서 @Lazy 를 사용하면 참조가 필요한 순간까지 빈 초기화를 대기시켜 애플리케이션이 정상적으로 실행될 수 있다.
@Component
public class ClassA {
private final ClassB classB;
public ClassA(@Lazy ClassB classB) { // @Lazy로 지연 초기화
this.classB = classB;
}
}
@Component
public class ClassB {
private final ClassA classA;
public ClassB(ClassA classA) {
this.classA = classA;
}
}
@RequiredArgsConstructor
@RequiredArgsConstructor 는 모든 final 필드와 @NonNull 필드를 포함한 생성자를 자동으로 생성해 주며, 순환참조와 생성자 주입 패턴을 함께 사용할 때 도움이 된다. 생성자 주입을 사용하면 순환참조가 발생하는 순간이 명확해지므로, 문제를 조기에 인지하고 해결하기 쉬워진다.
@RequiredArgsConstructor
@Component
public class ClassA {
private final ClassB classB; // final 필드를 생성자 주입
// @RequiredArgsConstructor 덕분에 생성자 자동 생성
}
@RequiredArgsConstructor
@Component
public class ClassB {
private final ClassA classA; // final 필드를 생성자 주입
}
여기서 @RequiredArgsConstructor 를 사용해 ClassA 와 ClassB 모두 생성자 주입을 통해 순환참조 관계를 해결할 수 있다. 생성자 주입은 필드 주입에 비해 순환참조를 인식하기 쉽고, @Lazy 와 결합하여 순환참조 문제를 더욱 효과적으로 우회할 수 있다.
인터페이스 도입
인터페이스를 사용하면 클래스가 직접적으로 다른 클래스를 참조하지 않고, 인터페이스를 통해 간접적으로 참조하게 되어 결합도가 낮아지고 순환참조를 방지할 수 있다.
→ 한 클래스에서 다른 클래스의 구현체를 참조하지 않고, 인터페이스에 의존하게 하여 의존성을 약화시킨다.
class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
ServiceA 와 serviceB 가 서로를 직접 참조하여 순환참조가 발생할 수 있다.
interface ServiceAInterface {
void someMethodInA();
}
interface ServiceBInterface {
void someMethodInB();
}
class ServiceA implements ServiceAInterface {
private final ServiceBInterface serviceB;
public ServiceA(ServiceBInterface serviceB) {
this.serviceB = serviceB;
}
@Override
public void someMethodInA() {
// ServiceB를 사용한 로직
}
}
class ServiceB implements ServiceBInterface {
private final ServiceAInterface serviceA;
public ServiceB(ServiceAInterface serviceA) {
this.serviceA = serviceA;
}
@Override
public void someMethodInB() {
// ServiceA를 사용한 로직
}
}
이렇게 ServiceA 와 ServiceB가 서로의 구체적인 클래스가 아닌 인터페이스에 의존하도록 만들어 순환참조 문제를 해결할 수 있다.
ServiceA → ServiceBInterface
ServiceB → ServiceAInterface
spring.main.allow-circular-references=true (권장되지 않음)
spring.main.allow-circular-references=true 설정은 Spring Boot 애플리케이션에서 순환 참조를 허용하는 설정 옵션이다. 기본적으로 Spring 5.3 부터는 순환 참조를 허용하지 않으며, 순환 참조가 감지되면 애플리케이션이 실행 중단된다. 하지만, 이 설정을 true 로 변경하면 순환 참조가 감지 되어도 Spring이 애플리케이션 실행을 계속하도록 허용할 수 있다.
yaml
spring:
main:
allow-circular-references: false
'Java > 기본' 카테고리의 다른 글
[Java] 참조란 무엇일까? (1) | 2024.11.14 |
---|---|
[Java] 컴파일 오류? 런타임 오류? (1) | 2024.11.14 |
[Java] @NoArgsConstructor, @AllArgsConstructor (0) | 2024.11.12 |
[Java] 트랜잭션 (0) | 2024.11.11 |
[Java] CompletableFuture (0) | 2024.11.08 |