애플리케이션 컨텍스트에서 일부 Bean의 종속성이 순환주기를 형성하는 문제가 발생함.
순환 종속성 또는 순환 참조 문제는 둘 이상의 Bean이 생성자를 통해 서로를 주입하려고할 때 발생한다.
accountService (1) → securityConfig (2) → formLoginAuthenticationProvider (3)
어플리케이션이 실행하면, 스프링 컨테이너는 3 → 2 →1 순으로 객체를 생성한다. 하지만 1 → 2 →3 순으로 객체들이 의존하기 때문에 어떤 객체를 먼저 생성해야하는지 문제가 발생한다. 이 경우 Spring은 컨텍스트를로드하는 동안 BeanCurrentlyInCreationException을 발생시킨다.
임시방편 해결 방법
FormLoginAuthenticationProvider
보통 의존성 주입은 @Autowired 또는, @RequiredArgsConstructor을 통한 주입을 할 것이다.
@Component @RequiredArgsConstructor public class FormLoginAuthenticationProvider implements AuthenticationProvider { private final AccountService accountService; private final PasswordEncoder passwordEncoder; ... }일반적으로 Spring은 애플리케이션 컨텍스트의 시작 / 부트스트랩(일반적으로 한 번 시작되면 알아서 진행되는 일련의 과정) 과정에서 모든 싱글톤 Bean을 즉시 생성한다. 그 이유는 런타임이 아닌 컴파일 과정에서 모든 가능한 오류를 즉시 피하고 감지하는 것이다.
위처럼 문제가 되는 빈들을 주입 받을 때, @Lazy 어노테이션을 통해 지연로딩으로 임시방편으로 문제를 해결한다.
어플리케이션이 실행하는 즉시 초기화가 아닌 필요에 따라 지연 해결 프록시(lazy-resolution proxy)가 주입되는 방식으로 사용한다.
@Component public class FormLoginAuthenticationProvider implements AuthenticationProvider { private AccountService accountService; private PasswordEncoder passwordEncoder; FormLoginAuthenticationProvider(@Lazy AccountService accountService, @Lazy PasswordEncoder passwordEncoder) { this.accountService = accountService; this.passwordEncoder = passwordEncoder; } ... }결론
임시방편으로 @Lazy 어노테이션을 통한 지연로딩으로 순환참조 문제를 해결했지만, 가장 이상적인건 스프링 빈들의 관계를 재설계해서 문제를 해결하는 것이다.
참고
//www.baeldung.com/circular-dependencies-in-spring
//docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html
스프링 부트부터 접한 스프링 알못이라 스프링에 대해 공부를 하다보니 너무나 모르고 있는 게 많아서 정리해봤다.
되게 간단한 건데 스프링 부트부터 접하면 몰라도 코드 짜는데는 문제가 없지만 개인적으로는 알고 있으면 너무나 좋은 내용같다.
어노테이션 없이 빈 설정
스프링이 관리하는 객체인 빈으로 생성하기 위해서 아래와 같은 어노테이션이 필수인 줄 알았다.
@Component, @Configuration, @Bean, @Service, @Controller, @Repository
하지만 직접 코딩을 해보니 이 생각은 거짓이었다.
우선 느슨한 결합을 위해 인터페이스를 하나 선언한다.
1 | public interface OrderService {} |
인터페이스의 구현체도 하나 만들어준다.
1 | public class TimonOrderService implements OrderService {} |
해당 구현체를 의존성으로 갖는 다른 구현체도 만들어보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class CoupangOrderService implements OrderService { private OrderService otherService; public CoupangOrderService(OrderService orderSe) { this.otherService = orderSe; } public OrderService getOtherService() { return otherService; } } |
이제 CoupangOrderService 빈이 제대로 생성되는지 테스트 코드를 작성해보자. (JUnit5를 사용하였다.)
1 2 3 4 5 6 7 8 9 10 11 12 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {CoupangOrderService.class}) class ApplicationContextTest { @Autowired private CoupangOrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); assertNotNull(coupangOrderService.getOtherService()); } } |
위 테스트를 실행하면 circular reference(순환 참조) 때문에 빈을 생성할 수 없는 오류가 난다.
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'coupangOrderService': Requested bean is currently in creation: Is there an unresolvable circular reference?
CoupangOrderService는 OrderService 인터페이스를 의존성으로 받는데 그 구현체가 CoupangOrderService 자신 밖에 없기 때문이다.
(@ContextConfiguration(classes = {CoupangOrderService.class})에 의해 ApplicationContext에서는 CoupangOrderService 밖에 모르기 때문이다.)
그럼 ApplicationContext가 OrderService의 다른 구현체인 TimonOrderService까지 알게 해주자.
1 2 3 4 5 6 7 8 9 10 11 12 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {CoupangOrderService.class, TimonOrderService.class}) class ApplicationContextTest { @Autowired private CoupangOrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); assertNotNull(coupangOrderService.getOtherService()); } } |
이제 테스트는 성공한다.
우리는 빈에 대한 어노테이션을 인터페이스나 구현체 어디에도 사용을 하지 않았는데 빈의 생성도 잘 이뤄졌고, 의존성 주입도 아주 잘 되었다.
CoupangOrderService에서 OrderService를 의존성 주입 받는데 OrderService의 구현체는 CoupangOrderService와 TimonOrderService 두 개이다.
하지만 스프링에서는 똑똑하게 순환참조 이슈를 피하려고 본인을 제외하고 빈을 찾기 때문에 순환참조 오류가 안 났다.
한 번 위 가설이 맞는지 검증해보자.
OrderService의 구현체를 하나 더 만들어보자.
1 | public class WeMakePriceOrderService implements OrderService {} |
이제 테스트를 돌려보면 아래와 같이 CoupangOrderService에 OrderService를 주입하는데 TimonOrderService를 주입해야할지, WeMakePriceOrderService를 주입해야할지 모른다는 오류가 나온다.
1 2 3 4 5 6 7 8 9 10 11 12 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {CoupangOrderService.class, TimonOrderService.class, WeMakePriceOrderService.class}) class ApplicationContextTest { @Autowired private CoupangOrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); assertNotNull(coupangOrderService.getOtherService()); } } |
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'OrderService' available: expected single matching bean but found 2: timonOrderService,weMakePriceOrderService
OrderService의 구현체는 세 개인데 당연스레 본인(CoupangOrderService)는 빼고 의존성 주입을 시도한 것이다.
그럼 위 테스트는 왜 실패한 것인가?
기본적으로 스프링은 아래와 같은 순서로 DI를 하게 된다.
- 빈의 타입으로 빈을 검색해서 주입한다.
- 해당 빈의 타입이 두 개 이상이면 빈의 이름으로 검색해서 주입한다.
1 2 3 | public CoupangOrderService(OrderService orderSe) { this.otherService = orderSe; } |
OrderService의 빈은 2개(CoupangOrderService 본인을 제외하고)라서 빈의 이름으로 검색을해야하는데 orderSe라는 이름의 빈은 없기 때문이다.
빈의 이름은 기본적으로 클래스 이름을 기반으로 생성된다. (규칙은 나중에 찾아보는 걸로…)
이제 테스트가 성공하게 제대로 된 빈의 이름으로 바꿔주자.
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class CoupangOrderService implements OrderService { private OrderService otherService; public CoupangOrderService(OrderService timonOrderService) { this.otherService = timonOrderService; } public OrderService getOtherService() { return otherService; } } |
빈 자동 스캔
우리가 생성한 빈이 많으면 많을 수록 @ContextConfiguration에 다 등록해주기도 부담이다.
이럴 때 쓰는 게 @Service, @Component, @Configuration, @Bean과 같은 어노테이션들이다.
우선 인터페이스와 구현체 어디다 쓰는 게 좋은지 모르니 다 붙여놓자.
1 2 | @Service public interface OrderService {} |
1 2 | @Service public class TimonOrderService implements OrderService {} |
1 2 | @Service public class WeMakePriceOrderService implements OrderService {} |
1 2 3 4 5 6 7 8 9 10 11 12 | @Service public class CoupangOrderService implements OrderService { private OrderService otherService; public CoupangOrderService(OrderService timonOrderService) { this.otherService = timonOrderService; } public OrderService getOtherService() { return otherService; } } |
그리고 빈을 자동으로 스캔해주는 빈을 만들어주자.
1 2 | @ComponentScan("some.package") public class ComponentScanConfig {} |
해당 패키지에 있는 @Service, @Component, @Configuration, @Bean 요런 어노테이션들이 붙은 빈들은 자동으로 스캔하고 생성해주는 어노테이션이다.
(자세한 건 나중에 또 알아보자 ㅠㅠ)
이제 테스트에서 Bean 클래스들을 한땀 한땀 넣어주는 부분을 수정해보자.
1 2 3 4 5 6 7 8 9 10 11 12 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {ComponentScanConfig.class}) class ApplicationContextTest { @Autowired private CoupangOrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); assertNotNull(coupangOrderService.getOtherService()); } } |
Config 파일 하나로 코드가 너무나 쾌적해졌다.
이렇게 빈을 자동으로 스캔하고 생성할 때는 @ComponentScan 어노테이션이 엄청 큰 도움이 된다.
어노테이션은 인터페이스에? 구현체에?
1 2 | @Service public interface OrderService {} |
어노테이션을 인터페이스에만 붙이면 구현체 타입으로 DI를 받을 수 없다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {ComponentScanConfig.class}) class ApplicationContextTest { @Autowired private OrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); } } |
하지만 이번엔 빈을 생성하지 못한다는 에러가 나온다.
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'example.domain.OrderService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
왜냐면 인터페이스만 @Service 어노테이션을 붙여서 빈으로 생성이 되는데 인터페이스는 객체로 생성이 불가능하기 때문에 위와 같은 오류가 나는 것이다.
그럼 이번엔 구현체에만 @Service 어노테이션을 붙이면 어떻게 될까?
1 2 3 4 5 6 7 8 9 10 11 12 | @Service public class CoupangOrderService implements OrderService { private OrderService otherService; public CoupangOrderService(OrderService timonOrderService) { this.otherService = timonOrderService; } public OrderService getOtherService() { return otherService; } } |
이제 테스트를 고쳐보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {ComponentScanConfig.class}) class ApplicationContextTest { @Autowired private CoupangOrderService coupangOrderService; @Test void test() { assertNotNull(coupangOrderService); assertNotNull(coupangOrderService.getOtherService()); } } |
테스트를 돌리면 또 순환참조 오류로 실패한다.
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'coupangOrderService': Requested bean is currently in creation: Is there an unresolvable circular reference?
다른 OrderService 구현체에도 @Service 어노테이션을 붙여주자.
1 2 | @Service public class TimonOrderService implements OrderService {} |
1 2 | @Service public class WeMakePriceOrderService implements OrderService {} |
이제 테스트를 돌리면 정상적으로 돌아간다.
인터페이스에 어노테이션 안 붙여도 인터페이스 타입으로 느슨하게 결합해서 DI도 가능하고, 특정 구현체에 기능이 쓰고 싶다면 해당 구현체 타입으로 DI도 가능하고…
따라서 내가 봤을 때는 인터페이스에 어노테이션을 붙여놓는 건 딱히 의미가 없는 것 같다.