Setter주입은 정말 순환참조를 검증 안 할까?

홍혁준·2023년 4월 19일
5

배경

스프링에서 의존성 주입 방법에 대해 학습하던 도중 다음과 같은 글을 찾았습니다.

생성자 주입은 컴파일 직후 순환 참조를 검증해주지만,
setter 주입이나 filed 주입은 실제 순환참조된 값을 호출하기 전까진 이를 검증해주지 않는다.
그러니 생성자 주입을 쓰면 순환참조를 빠르게 막을 수 있어서 좋으니 생성자 주입을 사용하자.

검증

한 번 궁금해서 실행해봤습니다.

@Component
public class ClassA {

    private ClassB classB;

    @Autowired
    public ClassA(final ClassB classB) {
        this.classB = classB;
    }
}

@Component
public class ClassB {

    private ClassA classA;

    @Autowired
    public ClassB(final ClassA classA) {
        this.classA = classA;
    }
}

classA와 classB가 서로 참조하고 있습니다.

바로 위와 같은 에러가 터집니다. (친절하게 그림으로도 설명해주고 있습니다.)

보고 조금 감탄했습니다. 이런거까지 잡아주다니 역시 스프링

그래서 한번 setter 주입으로도 테스트 해봤습니다.

@Component
public class ClassA {

    private ClassB classB;

    @Autowired
    public void setClassA(final ClassB classB) {
        this.classB = classB;
    }
}

@Component
public class ClassB {

    private ClassA classA;

    @Autowired
    public void setClassA(final ClassA classA) {
        this.classA = classA;
    }
}

?????? setter 주입으로도 순환참조를 컴파일하자마자 잡아줍니다.

@Component
public class ClassA {

    @Autowired
    private ClassB classB;
}

@Component
public class ClassB {

    @Autowired
    private ClassA classA;
}

필드 주입으로도 순환참조를 컴파일하자마자 잡아줬습니다.

위 결과를 보고나서 “아 블로그는 역시 신뢰할 수가 없구나 Baeldung을 확인해봐야겠다.”라고 생각해 Baeldung으로 가 확인해봤습니다.

Baeldung에는 다음과 같이 적혀있습니다.

One of the most popular workarounds, and also what the Spring documentation suggests, is using setter injection.

순환참조가 좋은 형태는 아니지만, 프로젝트에 필요한 경우 setter 주입을 사용하면 순환참조 형태를 유지할 수 있다는 내용이었습니다. ????

그래서 저는 아 “Baeldung도 틀릴 때가 있구나, 역시 검증할땐 공식문서지”라고 생각해서 공식문서를 찾아봤습니다.

If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.

For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.

One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.

요약하자면, 생성자 주입을 사용하면 순환참조를 검증해서 BeanCurrentlyInCreationException을 throw한다는 내용이었습니다. 그리고순환참조 관계를 유지하기 위해서는 setter 주입을 쓰는 방법이 있다고 했습니다.

그래서 이전 버전의 공식문서를 보고있어서 위처럼 나와있나 싶어서 가장 최신 문서인 스프링 6이상의 문서를 보았는데도 동일하게 위와 같은 내용이 적혀있었습니다.

그래서 내린 결론은 Spring에서 검증해주는게 아니다! 였고, 다음으로 눈을 돌린 것은 Spring boot 였습니다.

Spring boot 2.6 릴리즈노트에 보면 순환참조를 디폴트로 잡아준다는 내용이었습니다. 실제로 스프링 부트 2.5이하로 실행하니 setter나 필드 주입을 하였을 때 컴파일하자마자 Exception이 발생하진 않았습니다.

그럼 왜 스프링에선 검증하지 않고, 부트에선 검증할까?

일단 어떻게 Spring에서 생성자 주입은 실행하자마자 순환참조를 검증하고, setter주입이나 필드 주입은 그렇지 않는지 알아야 합니다.

공식문서에선 다음과 같이 의존관계 주입을 한다고 합니다.

Spring sets properties and resolves dependencies as late as possible, when the bean is actually created.

실제로 스프링 빈들은 미리 생성해둘 뿐, 의존관계가 필요하기 전까지 주입을 해주지 않는다고 합니다. 그렇기에 컴파일 직후에는 setter주입이나 필드주입으로 생기는 순환참조를 검증하지 못하는 것입니다.

하지만, 객체를 생성하는 것은 컴파일 할 때 일어나는 일입니다. 스프링 빈을 등록하는 과정 자체에서 의존관계를 다 주입을 해버리니, 컴파일 때 생성자 주입이 검증이 되는 것입니다.

그렇다면, 부트는 setter 주입이나 생성자 주입으로 만들어지는 순환참조를 어떤식으로 검증할까요?

이 부분은 개인적인 추측입니다. 어떻게 검증하는지는 찾아볼 수 없는데, 실제 스프링 부트 코드를 뜯어보는 것은 제 역량이 부족하기에 후술할 내용은 개인적인 추측입니다.

먼저 스프링이 어떻게 IoC 컨테이너에 빈을 등록하는지부터 알아야 합니다.

The configuration metadata is represented in XML, Java annotations, or Java code. It lets you express the objects that compose your application and the rich interdependencies between those objects. - spring 공식 문서

스프링 공식문서에 보면 IoC컨테이너는 빈을 등록할 때, XML, annotations, java code에서 빈으로 설정할 의존관계와 같은 메타데이터를 받는다고 합니다.

즉, 스프링 IoC 컨테이너는 이미 등록된 빈 객체들의 의존관계를 다 알고있는 것입니다. 그렇기에 추후에 필요할 때에 의존관계를 주입해주는 것이겠죠.

아마 Spring boot에서는 IoC컨테이너에서 의존관계에 대한 정보를 받아 막아주는 것으로 추측됩니다.(Spring에서도 충분히 막으려면 막을 수 있었을 듯?)

요약

스프링에서는 이전에 올라온 글들처럼 setter나 필드 주입으로 의존성을 주입하는 경우 컴파일하자마자 순환참조를 검증해주지 않습니다.

하지만, 스프링 부트 2.6 이상을 이용하면 생성자 주입이나, setter 주입이나 똑같이 순환참조를 검증하고 Exception을 발생시켜줍니다.
순환참조를 사용하고 싶다면 설정에서 spring.main.allow-circular-refernces를 true로 설정하면 됩니다.

글에 대한 지적과 질문은 언제든지 환영입니다,

profile
끊임없이 의심하고 반증하기

1개의 댓글