[오늘 만난 오류] 스프링 빈 순환 참조

Updated:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   memberController defined in file [/Users/sanha/SpringStudy/promisor/out/production/classes/promisor/promisor/domain/member/api/MemberController.class]
┌─────┐
|  memberService defined in file [/Users/sanha/SpringStudy/promisor/out/production/classes/promisor/promisor/domain/member/service/MemberService.class]
     
|  webSecurityConfig defined in file [/Users/sanha/SpringStudy/promisor/out/production/classes/promisor/promisor/global/config/WebSecurityConfig.class]
     
|  jwtProvider defined in file [/Users/sanha/SpringStudy/promisor/out/production/classes/promisor/promisor/global/config/security/JwtProvider.class]
└─────┘

애플리케이션 컨텍스트에서 일부 Bean의 종속성이 순환주기를 형성하는 문제가 발생하였다.

순환 참조 분제는 둘 이상의 Bean이 생성자를 통해 서로를 주입하려고 할 때 발생한다.

memberService (1) → webSecurityConfig (2) → jwtProvider (3)

어플리케이션이 실행하면 스프링 컨테이너는 3 → 2 → 1 순으로 객체를 생성한다.

하지만 1 → 2 → 3 순으로 객체들이 의존하기 때문에 어떤 객체를 먼저 생성해야 하는지 문제가 발생한다.

스프링은 컨텍스트를 로드 하는 동안 BeanCurrentlyInCreationException을 발생시킨다.

@Component
@RequiredArgsConstructor
public class JwtProvider {

    private final String secretKey = "sd92dfs0-1544-32da-bd21-bd234slkj";
    private final Long accessExpireTime = 60 * 60 * 1000L; // 3시간
    private final Long refreshExpireTime = ((60 * 60 * 1000L) * 24) * 60; // 60일
    private final MemberService memberService;

생성자 주입 방식의 경우, 스프링빈을 등록할 때, 의존관계 주입이 같이 일어난다.

애플리케이션 컨텍스트의 시작 과정에서 모든 싱글톤 빈을 즉시 생성하며 컴파일 과정에서 모든 가능한 오류를 즉시 파악한다.

@Lazy 에너테이션을 사용하여 지연로딩을 통해 임시 방편으로 문제를 해결하였다.

어플리케이션이 실행하는 초기화가 아닌 필요에 따라 lazy-resolution proxy가 주입되는 방식으로 사용한다.

@Component
public class JwtProvider {

    private final String secretKey = "sd92dfs0-1544-32da-bd21-bd234slkj";
    private final Long accessExpireTime = 60 * 60 * 1000L; // 3시간
    private final Long refreshExpireTime = ((60 * 60 * 1000L) * 24) * 60; // 60일
    private final MemberService memberService;

    public JwtProvider(@Lazy MemberService memberService) {
        this.memberService = memberService;
    }

하지만 이 방식은 임시 방편에 불과할 뿐이다.

스프링 빈 순환 참조 문제를 해결하는 방법은 여러 가지가 있는데 그중에서 제일 먼저 고려해야 할 해결법은 컴포넌트들을 재설계하는 방법이다.

순환 참조 문제가 발생했다는 것은 보통 설계를 잘못했다는 의미이다. 각 객체들 간의 책임을 명확히 구분해주고 계층관계가 잘 설계되도록 디자인 해야한다.

기존의 설계방식의 문제점은 memberService에 너무 많은 책임을 위임했다는 것이었다.

memberService에 UserDetailsService 를 상속받아서 사용하고 있었는데 이러한 설계의 문제점은 loadUserByUsername 메서드를 사용하기 위해 WebSecurityConfig 객체와 JwtProvider 객체가 memberService에 의존하게 된다는 것이었다.

때문에 다음과 같은 순환 참조가 형성되었다.

"스프링빈 의존 사이클"

해결책

UserDetailsService 를 상속받는 CustomUserDetailService 객체를 별도로 생성해 주었다.

이로인해 WebSecurityConfig 객체와 JwtProvider 객체는 memberService를 의존하지 않고 CustomUserDetailService 에 의존하게 된다.

변경된 코드

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) {

        if (email.isBlank()) {
            throw new EmailEmptyException();
        }

        Optional<Member> optionalMember = memberRepository.findByEmail(email);
        Member member = optionalMember.orElseThrow(LoginInfoNotFoundException::new);

        Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(member.getRole()));
        return new User(member.getEmail(), member.getPassword(), authorities);
    }
}
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtProvider jwtProvider;
    private final WebAccessDeniedHandler webAccessDeniedHandler;
    private final CustomAuthenticationEntryPoint authenticationEntryPointHandler;
    private final CustomUserDetailService customUserDetailService;
    private final PasswordEncoder passwordEncoder;
@Component
@RequiredArgsConstructor
public class JwtProvider {

    private final String secretKey = "sd92dfs0-1544-32da-bd21-bd234slkj";
    private final Long accessExpireTime = 60 * 60 * 1000L; // 3시간
    private final Long refreshExpireTime = ((60 * 60 * 1000L) * 24) * 60; // 60일
    private final CustomUserDetailService customUserDetailService;

빈들간의 의존관계 또한 다음과 같이 개선되었다.

"스프링빈 의존 사이클"


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

Categories:

Updated:

Leave a comment