S E P H ' S

[Spring] 순환 참조에 대한 의문점 본문

Programing & Coding/Spring

[Spring] 순환 참조에 대한 의문점

yoseph0310 2023. 1. 10. 17:08

무지성으로 기능 구현에만 집중했던 프로젝트를 리팩토링하면서 여러가지 문제점들이 있다는 것을 알게 됐다. 그 덕에 열심히 다시 Spring에 대한 공부를 하고 있다. 지금 작성하고 있는 포스트에 대한 문제도 사실 해결을 못한 것은 아니다. 하지만 왜 이렇게 해결이 되었는지 문제는 왜 발생한건지 궁금해서 하나씩 다 열어보고 싶어졌다. 그래서 리팩토링을 거친 순서대로 한번 짚어보면서 내용을 정리해보기로 했다.

 

생성자 주입 방식

프로젝트 리팩토링 대상 중 하나는 바로 생성자 주입 방식으로 변경하는 것이었다. Spring 4.3 버전부터는 생성자 주입 방식을 권장하고 있고 그 이유에는 대표적으로 3가지가 있다. 먼저 이것부터 간단히 짚고 넘어가자.

 

1. 객체 불변성 확보

@Controller
public class Controller {
	private final Service service;
    
    @Autowired
    public Controller(Service service) {
    	this.service = service;
    }
}

 

위 코드에서 Service를 변경하는 코드는 Controller의 생성자 뿐이다. 그래서 생성자로 한번 주입하면 생성자는 최초 1회 이후 다시 생성될일이 없기 때문에 불변객체를 보장한다. 그리고 Controller가 생성되는 시점에 무조건 Service 객체가 생성되어 주입된다.

 

2. 테스트 용이

 

필드 주입으로 작성된 경우, 순수 자바 코드로 단위 테스트를 하는 것이 불가능하다. 메인 코드는 DI 프레임워크 위에서 동작하는데 단위테스트는 단독적으로 실행되기 때문에 의존 관계 주입이 null 상태여서 NullPointerException이 발생할 수 있다. 생성자 주입을 하게 되면 그 객체가 생성될 때 의존성 주입이 되기 때문에 문제없이 테스트가 가능하다.

 

3. 순환참조 에러방지

 

쉽게 말해, A객체는 B객체를 참조하고 B객체는 A객체를 참조하는 식으로 서로 동시에 참조하고 있을 때 순환 참조가 발생한다.

 

@Service
public class ServiceA {
	
    @Autowired
    private ServiceB serviceB;
    
    public void test() {
	serviceB.test();
    }
}


@Service
public class ServiceB {
	
    @Autowired
    private ServiceA serviceS;
    
    public void test() {
	serviceA.test();
    }
}

어플리케이션을 실행하고 A나 B에서 test를 호출하면 서로 메소드를 계속 호출하다가 StackOverFlow가 발생하면서 중지된다. 이때 치명적인것은 컴파일 시에는 아무 문제가 없다고 해놓고 메소드 호출시에 발생된다는 것이 문제이다. 생성자 주입 방식으로 이를 방지할 수 있는가? 그것보다는 순환 참조 에러 시점때문에 권장을 하는 것이다.

 

필드 주입과 수정자(setter) 주입은 프로그램 실행 중에 runtime 에러가 발생하게 되고 생성자 주입은 프로그램 실행 시점에 compile 에러가 발생한다. 즉, 필드 주입과 수정자 주입을 사용하면 개발자 입장에서는 컴파일 시에는 에러가 발생하지 않아서 기분 좋게 서비스를 배포했는데 에러가 후에 펑펑 터지는 모습을 보게 될 수 있고, 생성자 주입은 서비스가 되기 전, 에러를 발견 하기 때문에 이를 수정할수 있도록 유도한다. 사실 순환 참조가 일어나지 않게 객체 설계를 하는 것이 가장 중요하기는 하다.

 

즉, 외부에 의해 변경되지 않아야 할 필요가 있고 테스트를 용이하게 하고 순환 참조를 미리 발견할 수 있게 하기 위해 생성자 주입을 사용하는 것인데 나에게 궁금증은 이 생성자 주입 방식을 SecurityConfig를 참조하는 UserService를 리팩토링 하면서 생겨났다.


의문점

위와 같은 사실을 알게 되고 하나씩 차근차근 생성자 주입 방식으로 코드를 변경하고 있었다. UserService 부분 부터 변경하기 시작했는데 여기서부터 문제가 발생했다.

UserService.java

이 UserService를 주입받아 사용하는 UserDetailsService, SecurityConfig에서 순환 참조가 발생하게 됐다.

순환 참조 에러 발생
SecurityConfig.java
PMUserDetailsService.java

내 생각으로는 이해가 되지 않던 부분은 PasswordEncoder를 주입 받을때 위와 같은 순환 참조가 발생했다. PasswordEncoder는 SecurityConfig에서 Bean으로 등록되있다. SecurityConfig는 또 UserService를 주입받아서 사용하고 있고 UserDetailsService 또한 마찬가지다.

 

이를 해결하기 위서 조사를 했다. 스프링 애플리케이션이 실행될 때 모든 Bean 객체는 초기화가 되는데 이때, 서로를 참조하고 있기 때문에 에러가 나는 것. 생성자 주입 방식을 권장하는 이유를 몸소 확인하게 된 사례였다. 그 때 눈에 들어온 것은 바로 @Lazy 어노테이션이다.

@Lazy는 Bean 즉시(Eager) 로딩이 아닌 지연(Lazy) 로딩을 위해 사용하는데 이는 실행될 때 모든 Bean을 초기화 하는 것이 아닌 다른 참조되는 Bean에 의해 사용되거나 실제 참조될 때 로드가 된다. 이를 활용해서 SecurityConfig의 생성자에 Lazy를 적용시키고 실행시켰더니 정상적으로 실행이 가능했다.

 

즉 의존성 주입을 할 때, 순환 참조가 발생하는 필드에 대해서 @Lazy 어노테이션을 적용하는 것(만약 모든 필드가 그러하다면 생성자 자체에 @Lazy를 사용하면 된다.)으로 순환 참조를 해결할 수는 있다. 이를 통해 얻어낸 개인적인 결론이 있다.

개인적인 결론

순환 참조를 해결하는 가장 좋은 방법은 역시 순환 참조가 발생하지 않는 설계를 기반으로 새롭게 설계를 하는 것이다. 일반적으로 단순히 생각해보더라도 서로를 참조하도록 만든 설계는 좋지 않은 설계이다. 그래서 나는 @Lazy 어노테이션을 사용하는 방식 대신 UserService를 사용하는 클래스들을 살펴보고 UserRepository로 대체할 수 있는 기능이면 그렇게 대체를 했다. 이 포스트에 나타난 문제도 말끔하게 해결할 수 있었다. 

 

재설계에 오랜 시간이 걸리거나 매우 어려운 상황이라면 @Lazy를 사용할 수는 있으나 무조건적인 사용은 지양해야할 것 같다고 생각했다. 이 포스트를 쓰기 위해 공부하던 중, 모 블로그에서 이런 문구를 봤다. 

마이크로서비스에서는 완전 자동화를 달성하기 위해 최소한의 시동/종료 시간을 갖도록 애플리케이션의 크기를 가능한 한 작게 유지하는 것이 극단적으로 아주 중요하다. 이를 위해 마이크로서비스에서는 객체와 데이터의 지연 로딩(Lazy Loading)에 대해서도 고려해보아야 한다.

여러 쓰임새가 있지만 적어도 순환 참조를 해결하기 위해 만들어진 것은 아니라는 것은 확실하고 엄연히 순환 참조가 없는 설계로 프로그래밍을 해야하는 것은 틀림없는 사실이다. 되도록 순환 참조가 발생하지 않는 코드를 작성해야 겠다고 생각하게 되었다. 또한 Lazy는 등록한 Bean이 많아서 로드하는데 시간이 오래걸리면 최적화의 용도로도 사용된다고 한다. 이런 부분이 마이크로서비스 최적화와 관련되어 있지 않을까라는 생각을 했다. 그리고 나는 마이크로서비스의 자동화를 공부하려는 목표가 있다. 이후 아마 다시 지연 로딩에 대해 찾아볼때 다시 만나게 될 것 같다.

 


포스트를 정리하면서 나를 포함해 순환 참조에 대해 고민하고 있을 분들에게 좋은 링크를 찾았다. 개인적으로 이 링크를 보면서 다시 한번 설계의 중요성을 깨닫게 되었다. 순환 참조 해결을 위해 대놓고 첫번째로

... ㅋㅋ

이렇게 말하고 있다. 적어도 새 프로젝트를 시작하거나 감당할 수 있다면 새로운 설계가 중요하다는 메세지인것 같다. 두번째 문단은 아마 이악물고 ㅎㅎ 어쩔수없죠 라고 말하는 듯 하다. 그외에도 다양한 해결방법이 소개되어 있으니 참고하면 좋을 듯 하다.

 

 

참고링크

https://www.baeldung.com/circular-dependencies-in-spring