java.lang.NullPointerException: Cannot invoke "org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder.getObject()" because "this.authenticationManagerBuilder" is null
🌱 문제 상황
기존에 구현했던 로직은 AuthenticationManagerBuilder를 통해 인증된 Authentication을 반환하는 형식이다.
실제 구동할 때는 잘 동작하는데, 단위 테스트를 수행할 때는 authenticationManagerBuilder.getObject()에서 NPE가 발생했다.
@RequiredArgsConstructor
@Service
public class AuthService implements UserDetailsService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@Transactional
public LoginResponseDto login(LoginRequestDto request) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
request.email(), request.password());
Authentication authentication = authenticationManagerBuilder.getObject()
.authenticate(authenticationToken); // 이 부분
// 토큰 발급 부분 생략
return new LoginResponseDto(account.getId(), token);
}
}
🤔 AuthenticationManagerBuilder는 무엇이고 어떻게 Authentication을 반환할까?
AuthenticationManagerBuilder는 인증 관련 설정을 구성하기 위해 Spring Security에서 사용되는 도구 중 하나이다.
Spring Boot는 실행될 때 자동 구성(auto-configuration)을 수행해 주로 클래스패스에 있는 설정들을 자동으로 감지하고 구성한다. AuthenticationManagerBuilder는 따로 설정을 해주지 않아도 애플리케이션 구동 시 Spring의 auth-configuration을 통해 자동으로 DI가 되어 간편한 사용이 가능하다. AuthenticationManagerBuilder.getObject() 메서드를 통해 AuthenticationManager로 실제 인증을 수행하는 것이다.
따라서 AuthenticationManagerBuilder를 따로 구현하지 않아도 의존성 주입이 되지만 단위 테스트 환경에서는 Mocking을 통해 전체 애플리케이션 컨텍스트를 로드하지 않기 때문에 auto-configuration이 적용되지 않아 getObject가 null이 되는 것이다.
@Test
@DisplayName("로그인에 성공하면 토큰을 얻는다")
void should_get_tokens_when_succeed_to_login() {
LoginRequestDto requestDto = new LoginRequestDto("email@gmail.com", "password1!");
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password());
given(authenticationManagerBuilder.getObject()).willReturn(authenticationManager);
given(authenticationManager.authenticate(any())).willReturn(authentication);
}
이렇게 getObject() 메서드가 호출될 때 AuthenticationManager 객체를 반환하도록 설정해도 똑같이 NPE가 발생했다.
🌱 해결 방법
AuthenticationManagerBuilder는 별다른 설정이 필요 없어 구현이 쉽지만 위에 설명한 문제 때문에 Mocking 하기 힘들다. 따라서 AuthenticationManagerBuilder를 의존성 주입 하는 대신, AuthenticationManager를 주입하고 Mocking 하게 되면 테스트 코드가 정상적으로 작동하게 된다.
AuthService.class
@RequiredArgsConstructor
@Service
public class AuthService implements UserDetailsService {
private final AuthenticationManager authenticationManager;
@Transactional
public LoginResponseDto login(LoginRequestDto request) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
request.email(), request.password());
// AuthenticationManagerBuilder 대신 AuthenticationManager로 수정
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 토큰 발급 부분 생략
return new LoginResponseDto(account.getId(), token);
}
}
SecurityConfig.class
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
// 추가
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 생략
}
}
AuthenticationManager는 인터페이스이기 때문에 따로 Bean을 등록해주어야 한다.
AuthServiceTest.class
@Test
@DisplayName("로그인에 성공하면 토큰을 얻는다")
void should_get_tokens_when_succeed_to_login() {
LoginRequestDto requestDto = new LoginRequestDto("email@gmail.com", "password1!");
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password());
given(authenticationManager.authenticate(any())).willReturn(authentication);
given(tokenProvider.createAccessToken(any())).willReturn(accessToken);
given(tokenProvider.createRefreshToken()).willReturn(refreshToken);
LoginResponseDto responseDto = authService.login(requestDto);
assertThat(responseDto.token().accessToken()).isEqualTo(accessToken);
assertThat(responseDto.token().refreshToken()).isEqualTo(refreshToken);
then(accountRepository).should(times(1)).findByUsername(any());
then(refreshTokenRepository).should(times(1)).save(any());
}
📚 참고
'Back-end > TroubleShooting' 카테고리의 다른 글
[Spring Boot] 동시성 문제 해결하기 (0) | 2024.06.15 |
---|---|
ConflictingBeanDefinitionException 그치만 중복된 빈이 없을때.. (0) | 2024.05.24 |
[Spring Security] AuthenticationException과 JWTException 분리 (0) | 2024.05.10 |
[Spring Security] OncePerRequestFilter.shouldNotFilter 잘 사용하기 (0) | 2024.05.10 |
[Spring Security] SecurityConfig permitAll() 적용 안 되는 이유 (0) | 2024.05.10 |