BackEnd/Spring Security

[Spring Security] SecurityConfig와 UserDetailsService에서 순환참조 문제 (Error creating bean with name 'securityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference?)

샤아이인 2023. 2. 8.

 

1. 문제의 상황

우선 Custom한 AuthenticationProvider를 구현하던 도중, 구현의 편의를 위해 @Autowired로 필요한 의존성들을 끌어다 사용 중 이었다. 또한 정상적으로 실행되고 있었다.

 

기능이 구현된 후에 단지 의존성 주입방식을 생성자 주입 방식으로 변경 테스트를 수행했는데 다음과 같은 에러가 발생하였다!

SecurityConfig <-> memberDetailsService(UserDetailsService) 간에 순환 참조 문제가 발생하게 된 것 이다!

우선 문제가 되고 있는 코드를 살펴보자!

 

▶ LoginAuthenticationProvider

@RequiredArgsConstructor
public class LoginAuthenticationProvider implements AuthenticationProvider {

    private final MemberDetailsService memberDetailsService;
    private final PasswordEncoder passwordEncoder;
    
    // authenticate 생략
}

우선 위 코드가 생성자 주입으로 변경된 Provider 이다.

 

▶ SecurityConfig

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...
    
    private MemberDetailsService memberDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberDetailsService).passwordEncoder(passwordEncoder());
    }

    ...
}

config에서는 memberDetailsService를 주입받아서 AuthenticationManagerBuilder를 통해 등록시키고 있다.

 

 

▶ MemberDetailsService

@Service
@Transactional
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    
    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member findMember = memberRepository.findByEmail(email)
                .orElseThrow(UserNotFoundException::new);

        return new MemberContext(findMember, findMember.getAuthorities());
    }
    
    // 회원 등록 메서드 생략...
}

memberDetailsService 에서는 memberRepository, passwordEncoder를 주입받아 빈을 생성한다.

 

2. 왜 발생한 것 일까?

그럼 어디서 순환참조가 발생하게 된 것 일까?

 

위 에러는 SecurityConfig의 configure method에서 memberDetailsService를 참조하고 있고,

memberDetailsService에서 PasswordEncoder를 참조하고 있고,

PasswordEncoder는 다시 SecurityConfig를 참조하기 때문에 참조 사이클이 생겨 에러가 발생한 것이다.

 

SecurityConfig -> memberDetailsService -> PasswordEncoder -> SecurityConfig

 

에러를 해결하기 위해 memberDetailsService를 회원 조회용으로 사용하고,

command성 메서드를 처리할 memberService를 따로 하나 구현하여

SecurityConfig에서 memberDetailsService가 아닌 해당 memberService를 참조하게 하면 해결된다.

 

에러를 해결하기 위해 다음과 같이 구현했다.

 

3. 해결 코드

▶ MemberDetailsService

@Service
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member findMember = memberRepository.findByEmail(email)
                .orElseThrow(UserNotFoundException::new);

        return new MemberContext(findMember, findMember.getAuthorities());
    }
}

더이상 memberDetailService에서 passwordEncoder를 의존하지 않는다.

또한 조회용 loadUserByUsername 말고는 구현된 메서드가 없다!

 

▶ MemberService

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void register(MemberRegisterRequest memberRegisterRequest) {
        checkAlreadyExistsUser(memberRegisterRequest.getEmail(), memberRegisterRequest.getUuid());
        Member newMember = createMember(memberRegisterRequest);
        memberRepository.save(newMember);
    }
    // 생략...
}

회원 등록용 메서드는 MemberService에서 담당하며, 여기서 PasswordEncoder를 의존하게 된다.

 

즉 기존에 다음과 같았다면

SecurityConfig -> memberDetailsService -> PasswordEncoder -> SecurityConfig

 

다음과 같이 변경되었다!

SecurityConfig -> memberDetailsService

 

더이상 순환 참조가 발생하지 않게 되었다!

댓글