Requested bean is currently in creation: is there an unresolvable circular reference?

Requested bean is currently in creation: is there an unresolvable circular reference?
2020-05-05 11:01:07.146 WARN 24213 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'accountController' defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/account/AccountController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'accountService' defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/account/AccountService.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/security/SecurityConfig.class]: Unsatisfied dependency expressed through constructor parameter 2; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'formLoginAuthenticationProvider': Unsatisfied dependency expressed through field 'accountService'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'accountService': Requested bean is currently in creation: Is there an unresolvable circular reference? *************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: accountController defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/account/AccountController.class] ┌─────┐ | accountService defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/account/AccountService.class] ↑ ↓ | securityConfig defined in file [/Users/seongjin/Documents/taggare/server/out/production/classes/com/sns/server/security/SecurityConfig.class] ↑ ↓ | formLoginAuthenticationProvider (field private com.sns.server.account.AccountService com.sns.server.security.providers.FormLoginAuthenticationProvider.accountService) └─────┘

애플리케이션 컨텍스트에서 일부 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 어노테이션을 통한 지연로딩으로 순환참조 문제를 해결했지만, 가장 이상적인건 스프링 빈들의 관계를 재설계해서 문제를 해결하는 것이다.

참고

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

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html

Requested bean is currently in creation: is there an unresolvable circular reference?

스프링 부트부터 접한 스프링 알못이라 스프링에 대해 공부를 하다보니 너무나 모르고 있는 게 많아서 정리해봤다.
되게 간단한 건데 스프링 부트부터 접하면 몰라도 코드 짜는데는 문제가 없지만 개인적으로는 알고 있으면 너무나 좋은 내용같다.

어노테이션 없이 빈 설정

스프링이 관리하는 객체인 빈으로 생성하기 위해서 아래와 같은 어노테이션이 필수인 줄 알았다.
@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. 해당 빈의 타입이 두 개 이상이면 빈의 이름으로 검색해서 주입한다.
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도 가능하고…
따라서 내가 봤을 때는 인터페이스에 어노테이션을 붙여놓는 건 딱히 의미가 없는 것 같다.

What is circular reference in Spring boot?

Circular dependency in Spring happens when two or more beans require instance of each other through constructor dependency injections. For example: There is a ClassA that requires an instance of ClassB through constructor injection and ClassB requires an instance of class A through constructor injection.

How does Spring overcome circular dependency?

A simple way to break the cycle is by telling Spring to initialize one of the beans lazily. So, instead of fully initializing the bean, it will create a proxy to inject it into the other bean. The injected bean will only be fully created when it's first needed.

How do you avoid circular dependencies?

To reduce or eliminate circular dependencies, architects must implement loose component coupling and isolate failures. One approach is to use abstraction to break the dependency chain. To do this, you introduce an abstracted service interface that delivers underlying functionality without direct component coupling.