티스토리 뷰
[Spring] @PostConstruct 내부에서 @Bean 메서드 호출로 인해 발생하는 circular dependencies 문제 해결하기
망나니개발자 2024. 9. 3. 10:00
1. @PostConstruct 내부에서 @Bean 메서드 호출로 인해 발생하는
circular dependencies 문제 해결하기
[ 문제 상황 공유 ]
예를 들어 다음과 같은 코드가 있다고 하자.
@Configuration
@Slf4j
public class ProfileConfiguration {
@Value("spring.profiles.active")
private String profile;
@PostConstruct
void init() {
ServerProfile serverProfile = serverProfile();
if (serverProfile != null) {
log.info("Using server profile: {}", serverProfile);
}
}
@Bean
public ServerProfile serverProfile(
) {
return new ServerProfile(profile);
}
@AllArgsConstructor
public static class ServerProfile {
private final String profile;
}
}
그리고 해당 ServerProfile을 MemberService에서 의존하고, MemberController는 MemberService에 의존한다고 하자. 이러한 구조에서 서버를 실행하면 다음과 같은 에러가 발생하게 된다.
The dependencies of some of the beans in the application context form a cycle:
memberController (field private com.mangkyu.MemberService com.mangkyu.MemberController.memberService)
↓
memberService (field private com.mangkyu.ProfileConfiguration$ServerProfile com.mangkyu.MemberService.serverProfile)
┌─────┐
| profileConfiguration
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
해당 에러가 발생하는 원인과 해결 방법에 대해 살펴보도록 하자.
[ 문제 발생 원인 파악 ]
언제나 문제 상황을 올바르게 파악하려면 에러 로그를 정확하게 볼 수 있어야 한다.
The dependencies of some of the beans in the application context form a cycle:
memberController (field private com.mangkyu.MemberService com.mangkyu.MemberController.memberService)
↓
memberService (field private com.mangkyu.ProfileConfiguration$ServerProfile com.mangkyu.MemberService.serverProfile)
┌─────┐
| profileConfiguration
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
위와 같은 에러 로그가 설명하는 것은 다음과 같다.
- memberController를 생성하는 도중에 memberService가 필요하여 이를 생성하고,
- memberService를 생성하는 도중에 profileConfiguration이 필요하여 이를 생성하다가,
- profileConfiguration 내부에서 순환 참고가 발생하고 있다.
여기서 중요한 것은 3번인데, profileConfiguration 내부에서 순환 참조가 발생한다는 것이다. 만약 profileConfiguration에서 memberController를 의존하는 상황이라서, 3개의 빈들 사이에 순환이 발생한다면 에러 로그가 다음과 같이 달라진다.
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| memberController (field private com.mangkyu.MemberService com.mangkyu.MemberController.memberService)
↑ ↓
| memberService (field private com.mangkyu.ProfileConfiguration$ServerProfile com.mangkyu.MemberService.serverProfile)
↑ ↓
| profileConfiguration (field com.mangkyu.MemberController com.mangkyu.ProfileConfiguration.memberController)
└─────┘
따라서 문제의 원인은 ProfileConfiguration 자체에 존재하는데, 그 원인은 @PostConstruct 메서드에서 @Bean 메서드를 호출하기 때문이다. 발생했던 전체 에러 로그는 다음과 같다.
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'profileConfiguration': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:409) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1167) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.resolveBeanReference(ConfigurationClassEnhancer.java:371) ~[spring-context-6.1.8.jar:6.1.8]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:342) ~[spring-context-6.1.8.jar:6.1.8]
at com.mangkyu.ProfileConfiguration$$SpringCGLIB$$0.serverProfile(<generated>) ~[main/:na]
at com.mangkyu.ProfileConfiguration.init(ProfileConfiguration.java:21) ~[main/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1780) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:409) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1167) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:784) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:767) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:508) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1421) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:784) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:767) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:508) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1421) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962) ~[spring-context-6.1.8.jar:6.1.8]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.8.jar:6.1.8]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.6.jar:3.2.6]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.6.jar:3.2.6]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.6.jar:3.2.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.2.6.jar:3.2.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.2.6.jar:3.2.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.2.6.jar:3.2.6]
at com.mangkyu.VirtualthreadApplication.main(VirtualthreadApplication.java:12) ~[main/:na]
그리고 여기서 우리에게 필요한 부분만 추려보면 다음과 같이 추릴 수 있다.
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'profileConfiguration': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at com.mangkyu.ProfileConfiguration$$SpringCGLIB$$0.serverProfile(<generated>) ~[main/:na]
at com.mangkyu.ProfileConfiguration.init(ProfileConfiguration.java:21) ~[main/:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1780) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
스프링의 메서드 네이밍은 매우 직관적이기 때문에, 어떠한 상황인지 어느 정도 유추할 수 있다.
- profileConfiguration 빈을 만드려고 함(getBean, createBean)
- profileConfiguration 빈이 생성된 후에, 후처리를 함(postProcess = @PostConstruct 처리)
- 그 과정에서 다시 빈이 필요하여 생성 시도함(getBean, createBean)
- 현재 생성중인 상태의 빈(profileConfiguration)이 다시 생성 호출되므로 순환 참조임
따라서 문제는 ProfileConfiguration의 @PostConstruct에서 @Bean 메서드를 호출하여, 현재 생성중인 ProfileConfiguration 빈을 다시 생성하려고 시도하여 발생함을 알 수 있다.
[ 문제 해결하기 ]
설정을 추가하여 순환 참조 허용하기
순환 참조에 의한 문제를 가장 간단하고 빠르게 해결하는 설정을 추가하여 이를 허용하는 것이다. 프로퍼티 파일에 다음과 같은 설정을 추가하면 문제를 바로 해결할 수 있다.
spring.main.allow-circular-references=true
하지만 다음과 같은 에러 메시지에서 보이듯이 순환 참조는 지양해야 하며 스프링 부트 2.6부터 기본적으로 막혀있다. 따라서 다른 해결 방법을 찾아야 한다.
Relying upon circular references is discouraged and they are prohibited by default
의존성을 분리하기
가장 현명하면서도 추구해야 하는 해결책은 의존 관계를 변경하여 이러한 사이클을 제거하는 것이다. 외부에서 Profile 부분의 빈을 생성하도록 변경하고 ProfileConfiguration에서는 이를 주입받는 것이다.
따라서 @PostConstruct 내부에서 bean 메서드 호출을 제거하여 문제를 해결할 수 있다.
@Slf4j
@Configuration
@AllArgsConstructor
public class ProfileConfiguration {
private final ServerProfile serverProfile;
@PostConstruct
void init() {
log.info("Using server profile: {}", serverProfile);
}
}
[ 스프링 공식 문서 살펴보기 ]
스프링은 공식 문서에서 개발자들이 겪을 수 있는 많은 문제 케이스들에 대하여 잘 정리해두었다. 해당 부분 역시 공식 문서에 친절하게 적혀있다. 공식 문서에서 @PostConstruct 내부에서 동일한 구성 클래스의 빈 정의 메서드 호출을 하게 되면 순환 참조가 발생할 수 있으니 주의하라고 한다.
가장 간단한 해결책은 프로퍼티 하나를 추가하는 것일 수 있다.
하지만 이러한 깨진 유리창을 하나씩 허용하는 순간부터 우리의 프로젝트는 더럽혀지기 시작할 것이며, 이로 인해 남은 멀쩡한 유리창이 없어질 것이다. 또한 이러한 에러를 보고 문제를 파악하는 것부터 해결까지, 처음에는 어렵지만 이러한 반복적인 수련을 통해서 우리가 한 단계 성장할 수 있을 것이다.
따라서 이후에도 이런 비슷한 상황이 생긴다고 하더라도, 문제의 발생 원인부터 해결까지 시도해보도록 하자. 또한 스프링의 공식 문서는 매우 친절하게 작성되어 있으니 친하게 지내도록 하자.