본문 바로가기

Spring&IntelliJ

[Spring] 순환 참조(Circular References) 해결하기

인용: https://velog.io/@platinouss/Spring-Circular-References-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0

이와같은 순환참조 문제에 직면하였다.

내가 아래글을 이해하기 위해서 필요한 것은 @Configuration 역시 @Component의 일종으로 컴포넌트 스캔시 빈 컨테이너에 포함된다는 것이다. 또한 아래와 같은 코드 구조에서 SecurityConfig를 빈에 등록하기 위해서는 authenticationProcider가 빈객체에 미리 존재하여야만 한다는 것이다.

@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AuthenticationProviderService  authenticationProvider;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 생략된 코드
}

만약 아래와 같이 @RequiredArgsConstructor 와  private final AuthenticationProviderService  authenticationProvider;  가 없다면 어떠한 재약사항도 없이 SecurityConfig  클래스를 빈에 등록할 수 있고, 빈에 등록하면 그에따라 BCryptPasswordEncoder  도 빈에 등록되게 된다는 것이다.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 생략된 코드
}

SecurityConfig를 빈에 등록하기 위해서는 AuthenticationProviderService가 필요하다. AuthenticationProviderService를 빈에 등록하기 위해서는 사전에 SecurityConfig 빈이 생성되어 있어야 BCryptPasswordEncoder이 빈으로 생성될 수 있다. 즉 , SecurityConfig 클래스와 authenticationProviderService 클래스가 서로 순환 참조를 하게되는 구조이다.

즉 A(SecurityConfig)를 확보하기 위해서는 B(AuthenticationProviderService)가 필요한데 B(AuthenticationProviderService) 를 확보하기 위해서는 A (SecurityConfig) 를 확보해야 얻을수 있는 C(BCryptPawordEncoder)가 필요한 것이다.

SecurityConfig =>> AuthenticationProviderService  =>> BCryptPasswordEncoder  =>> SecurityConfig

결국 어떠한 Bean도 생성하지 못하는 문제가 발생된다.

@Lazy 활용과 같은 일시적인 방법 등이 있지만, 설계 원칙에 따라 적합한 방식은 순환 참조 고리를 끊도록 재설계 하면 된다.

 

(참고로 위의 문제를 그림으로 표현하면 아래와 같다. SecurityConfig 는 AuthenticationProviderService 를 필요로 하고

 AuthenticationProviderService는 SecurityConfig 를 필요로 하는 구조다.

)

 

해당 문제의 순환 참조 고리를 끊는 가장 적합한 방법은 bCryptPasswordEncoder를 새로운 @Configuration 클래스의 Bean으로 등록하는 것이다. 그렇게 되면 다음과 같은 구조가 만들어지고, 자연스레 순환 참조의 고리가 끊어지게 된다.

/**
 * SecurityConfig.java
 */
 
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AuthenticationProviderService authenticationProvider;

    // 생략된 코드
}

 

/**
 * appConfig.java
 */
 
@Configuration
public class AppConfig implements AuthenticationProvider {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
			SecurityConfig -> AuthenticationProviderService -> AppConfig

 

참고1.

참고2.https://hungrydiver.co.kr/bbs/detail/develop?id=90

순환 종속성을 해결하는 해결책 중 하나는 일부 종속빈을 생성자가 아닌 setter를 사용하도록 수정하는 것입니다. 또는 전체적으로 생성자 주입을 피하고 Setter 주입 만 사용하는 것입니다. 권장되지는 않지만 setter 주입으로 동작하는 순환 종속성이 동작하게는 할 수 있습니다

 

-------------------------------아래가 내가 실제 직면한 문제에 대한 정답임---------------------------------------------------

직접 @Bean으로 등록했을 때 순환참조가 발생하는 이유는 다음과 같습니다.

@Configuration

public class SpringConfig {

    @Bean
    public TimeTraceAop timeTraceAop() {
        return new TimeTraceAop();
    }
}

@Aspect
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { }
}

TimeTraceAop의 AOP 대상을 지정하는 @Around 코드를 보시면, SpringConfig의 timeTraceAop() 메서드도 AOP로 처리하게 됩니다. 그런데 이게 바로 자기 자신인 TimeTraceAop를 생성하는 코드인 것이지요. 그래서 순환참조 문제가 발생합니다.

반면에 컴포넌트 스캔을 사용할 때는 AOP의 대상이 되는 이런 코드 자체가 없기 때문에 문제가 발생하지 않았습니다.

그러면 AOP 설정 클래스를 빈으로 직접 등록할 때는 어떻게 문제를 해결하면 될까요? 바로 다음과 같이 AOP 대상에서 SpringConfig를 빼주면 됩니다.

@Aspect
public class TimeTraceAop {


    @Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")

    //@Around("execution(* hello.hellospring..*(..))")

    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {...}

}

 

위글을 통해 또 하나 배운점.

"클래스를 빈으로 직접 등록" 한다는 것은 어떠한설정파일, 예를들면 내 프로젝트의SpringConfiguration의@Configuration 어노테이션아래에 @Bean어노테이션을 붙여서 매서드의 반환형으로 그 클래스의 생성자를 두는 것이다. 예를들면 아래와 같은 형식이다. 즉 @Configuration 은 클래스를 대상으로 @Bean은 메서드를 대상으로 쓰인다. 빈으로 등록하는 것은 메서드를 통한 객체의 반환형 형식이다.