[F-Lab 챌린지 24일차] 스프링에서 순환참조

성수데브리·2023년 7월 22일
0

f-lab_java

목록 보기
20/73

스프링 컨테이너는 어느 객체부터 생성할까?
스프링은 로딩한 Bean들이 완전히 작동할 수 있는 순서대로 생성한다고 한다.

A 는 B 가 필요하니 B 부터 생성하려 하지만 B 는 C 가 필요하니 C부터 생성한다.
C → B → A 가 생성된다.

순환 참조란

객체 A, B, C 가 있다. 이렇게 참조 방향이 순환하고 있는 형태를 순환 참조라 한다.

스프링에서 순환 참조가 있으면 발생하는 문제

  1. Bean 등록 시점

스프링 컨테이너가 빈을 생성하는 방법대로 생성 순서를 따져보자.

A 는 B 가 필요하다. B 는 C 가 필요하다. C 는 A 가 필요하다.
A 는 B 가 필요하다. B 는 C 가 필요하다. C 는 A 가 필요하다.

빈 생성이 무한 루프에 빠진다.

하지만 스프링은 순환참조를 인지하는 로직이 구현되어 있다. 단, 생성자 주입일 경우에 가능하다.

생성자 주입을 사용할 경우 순환 참조 인지

BeanA → BeanB, BeanB → BeanA 생성사 주입으로 순환 참조를 만들고 어플리케이션을 실행해보자.

@Component
public class BeanA {
    private BeanB beanB;

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

    private BeanA beanA;

    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}

스프링이 순환 참조를 인지하고 어플리케이션 로딩을 중단했다.


***************************
APPLICATION FAILED TO START
***************************
┌─────┐
|  beanA defined in file [BeanA.class]
↑     ↓
|  beanB defined in file [BeanB.class]
└─────┘

디버깅을 해보면 생성된 싱글톤 빈을 등록하는 DefaultSingletonBeanRegistry.classgetSigleton() 메서드 내부에서 beforeSigletonCreation(beanName) 을 호출한다.
이름을 보니 뭔가 빈을 등록하기 전 무언가를 수행하는 것 같다.

beforeSigletonCreation(beanName) 로 이동하니 2가지 조건을 충족하면
BeanCurrentlyInCreationException 예외를 뱉는다.
이미 생성중인 Bean 을 또 생성하려해서 예외를 발생시키고 있다.

inCreationCheckExclusions 은 빈 생성 중에 검사를 제외할 빈의 이름들을 담는 Set 이다.

singletonsCurrentlyInCreation 은 현재 생성 중인 빈들의 이름을 저장하는 Set 이다.

inCreationCheckExclusions 에서 beanA 는 검사 제외 대상에 없고

singletonsCurrentlyInCreation 현재 생성 중인 빈들의 이름중에 이미 beanA 가 있다.

이렇게 두 조건을 충족해서 BeanCurrentlyInCreationException 예외를 발생시켰다.

Setter 주입에서는 순환참조 인지가 불가능한 이유

Setter 주입은 setter 가 호출되는 시점에 의존성 주입을 하기 때문에 알 수 없다.

@Component
public class BeanA {
    private BeanB beanB;

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

@Component
public class BeanB {

    private BeanA beanA;

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

그럼 발생할수 있는 문제는 무엇일까?

@Component
public class BeanB {

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

    public void call() {
        beanA.call();
    }
}

@Component
public class BeanA {

    @Autowired
    private BeanB beanB;

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

    public void call(){
        System.out.println("BeanA.call");
        beanB.call();
    }
}

beanA.setBeanB(beanB);
beanB.setBeanA(beanA);

beanA.call();
beanB.call();

순환 참조 관계에 있는 객체가 무한히 서로의 메서드를 호출할 수 있다. 역시 StackOverFlow 발생한다.

의존성 주입 방법 장단점

  1. 생성자 주입

    장점

    1. 순환참조를 앱 구동시에 알 수 있다. 순환참조로 인한 런타임 장애를 막을 수 있다.
    2. 생성자 주입은 객체 생성 시 최초 초기화하고 이후엔 변경되지 않으므로
      immutable 한 객체를 만들 수 있다. final 키워드 선언이 가능하다.

    단점

    1. 의존성 주입 대상 변경을 못한다.
  2. setter 주입

    장점

    1. 의존성 주입 대상을 변경할 수 있다.

    단점은

    • NPE 발생 위험이 있다.
      만일 코드에서 setter 로 주입되는 객체가 Null 이고 주입된 객체 참조값을 사용하는 코드가 있다면 NPE 가 발생한다.
    • 숨겨진 의존성
      생성자나 필드 주입방식 처럼 바로 의존 관계를 파악하기 어렵다.
      개발자가 의존성이 주입되는 메서드를 직접 찾아야 하는 단점이 있다.
    • immutable 하지 못하다. final 키워드 선언 불가능하다.
  1. field 주입

    장점

    1. 의존성 주입 대상을 변경할 수 있다.

    단점

    1. immutable 하지 못하다.

      Autowired Fields

      Fields are injected right after construction of a bean

      필드에 선언된 @Autowired 어노테이션 프로세서는 객체가 생성된 직후 의존성 주입을 하기 때문에
      final 키워드 선언이 불가능하다.

확인이 필요한 것

필드 주입 방식도 앱 구동시 순환참조 인지가 불가능하다고 봤는데 코드를 직접 짜보니

스프링이 순환 참조가 있으니 앱 구동을 중지했다. 이건 더 알아봐야겠다.

@Component
public class BeanB {
    @Autowired
    private BeanA beanA;
}

@Component
public class BeanA {
    @Autowired
    private BeanB beanB;
}
┌─────┐
|  beanA (field private BeanA.beanB)
↑     ↓
|  beanB (field private BeanB.beanA)
└─────┘

참고

https://www.linkedin.com/pulse/you-should-stop-using-spring-autowired-felix-coutinho/

https://www.baeldung.com/java-spring-field-injection-cons

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

정보 감사합니다.

답글 달기