문제 상황
잘못된 아이디로 로그인 요청을 보낸 경우, 401 Unauthorized 상태 코드가 반환되어야 하는데 500 Internal Server Error가 발생 !
2025-04-26T04:21:15.070+09:00 ERROR ... : Exception 발생: 사용자를 찾을 수 없습니다.
org.springframework.security.authentication.InternalAuthenticationServiceException: 사용자를 찾을 수 없습니다.
Caused by: com.deepnyangning.capstonebe.global.exception.CustomException: 사용자를 찾을 수 없습니다.
기존 로그인 요청 처리 기대 플로우
1. 클라이언트 요청
- 클라이언트가 /auth/login 엔드포인트에 POST 요청을 보냄
2. 컨트롤러
- AuthController의 login 메서드가 요청을 받아 AuthService.login 호출
3. AuthService
- Spring Security의 AuthenticationManager를 사용해 인증 시도
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getIdentifier(), request.getPassword())
);
- 인증 성공 시 CustomDetails를 추출하고, JWT 엑세스/리프레시 토큰을 생성하여 LoginResponse를 반환
- 인증 실패 시 BadCredentialsException을 캐치하여 CustomException(ErrorCode.INVALID_CREDITIALS)로 변환
catch (BadCredentialsException e){
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
4. CustomUserDetailsService
- AuthenticationManager는 내부적으로 CustomUserDetailsService.loadUserByUsername을 호출하여 사용자 정보를 로드
- 이 메서드는 identifier(username)를 기반으로 데이터베이스에서 사용자를 조회
User user = userRepository.findByIdentifier(username)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
5. 예외 처리
- 인증 실패 시 발생한 예외는 GlobalExceptionHandler에서 처리
- CustomException은 handleCustomException을 통해 USER_NOT_FOUND ErrorCode에 따라 401 상태 코드로 처리되어 ErrorResponse를 반환
@ExceptionHandler(CustomException.class)
protected final ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
ErrorResponse response = ErrorResponse.builder()
.code(e.getErrorCode().getHttpStatus().value())
.error(e.getErrorCode().getHttpStatus().name())
.message(e.getErrorCode().getMessage())
.build();
return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response);
}
6. 클라이언트 응답
- 성공 : 200 OK | LoginResponse {access + refresh token}
- 실패 : 401 Unauthorized | ErrorResponse {"code": 401, "error": "UNAUTHORIZED", "message": "아이디 또는 비밀번호가 올바르지 않습니다."}
문제 발생 지점
잘못된 아이디로 로그인 요청을 보냈을 때, login 메서드 내부에서
BadCredentialsException이 발생하여 GlobalExceptionHandler에 의해 401 UNAUTHORIZED가 반환되기를 기대했지만,
InternalAuthenticationServiceException이 발생하면서 GlobalExceptionHandler에 의해 500 INTERNAL_SERVER_ERROR가 클라이언트로 반환됨
문제 원인
CustomUserDetailsService의 loadUserByUsername에서 사용자가 존재하지 않을 경우 CustomException(ErrorCode.USER_NOT_FOUND)를 던지고 있던 것 !
User user = userRepository.findByIdentifier(username)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
1. CustomUserDetailsService의 예외
- 프로젝트에서 CustomException을 통해 ErrorCode를 정의하여 일괄적으로 처리하고 있었기 때문에 여기서도 CustomException을 통해 처리했는데 loadUserByUsername 메서드는 Spring Security가 처리하는 것..
- Spring Security는 UserDetailsService에서 UsernameNotFoundException을 기대하여 로그인 실패 시 BadCredentialsException으로 변환
- UsernameNotFoundException이 아닌 CustomException이 발생하면 비표준 예외로 처리됨
2. Spring Security의 예외 매핑
- CustomException은 Spring Security의 DaoAuthenticationProvider에서 InternalAuthenticationServiceException으로 래핑
Caused by: com.deepnyangning.capstonebe.global.exception.CustomException: 사용자를 찾을 수 없습니다.
at com.deepnyangning.capstonebe.domain.user.service.CustomUserDetailsService.lambda$loadUserByUsername$0(CustomUserDetailsService.java:22)
- InternalAuthenticationServiceException은 인증 과정의 내부 오류로 간주됨 → Spring Security가 이를 BadCredentialsException으로 변환 X
3. AuthService.login의 제한된 예외 처리
- AuthService.login은 BadCredentialsException만 캐치하여 CustomException(ErrorCode.INVALID_CREDENTIALS)로 변환
catch (BadCredentialsException e) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
- InternalAuthenticationServiceException은 캐치되지 않고 상위로 전파됨!
4. GlobalExceptionHandler의 처리
- 전파된 InternalAuthenticationServiceException은 handleGeneralException에서 캐치되어 500 에러로 처리됨
@ExceptionHandler(Exception.class)
protected final ResponseEntity<ErrorResponse> handleGeneralException(Exception e) {
ErrorResponse response = ErrorResponse.builder()
.code(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error(HttpStatus.INTERNAL_SERVER_ERROR.name())
.message("서버 오류가 발생했습니다.")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
- 원래 의도였던 handleCustomException은 호출되지 않음 . .
문제 해결
CustomUserDetailsService를 수정하여 Spring Security의 표준 예외 처리 흐름을 따르도록 함
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByIdentifier(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자(" + username + ")를 찾을 수 없습니다."));
return new CustomUserDetails(user);
}
→ Spring Security는 로그인 실패가 발생하면 BadCredentialsException으로 처리 가능
→ AuthService는 BadCredentialsException를 받아 CustomException(ErrorCode.INVALID_CREDENTIALS)을 던짐
→ GlobalExceptionHandler는 401 Unauthorized로 클라이언트 응답

Spring Security는 UsernameNotFoundException을 기준으로 인증 실패를 판단하므로, 커스텀 예외를 던지면 예외 흐름이 깨질 수 있음. 따라서 UserDetailsService에서는 반드시 Spring Security 표준 예외를 따를 것!
'트러블슈팅' 카테고리의 다른 글
| @Retryable 사용 시 'Cannot locate recovery method' 500 에러 해결하기 (0) | 2026.01.15 |
|---|---|
| CustomException 처리 중 DB 저장 오류로 인한 500 에러 해결 (0) | 2025.04.29 |
| 예외 발생 시 로그 누락 - REQUIRES_NEW로 롤백 방지하기 (0) | 2025.04.29 |