Study/SpringBoot

Spring MVC와 Spring Security의 예외 처리 차이점

kanado 2025. 5. 5. 19:30

예외 처리란?

먼저 "예외 처리"는 프로그램 실행 중에 문제가 생겼을 때(예: 로그인 실패, 없는 사용자 요청 등), 그 문제를 적절하게 처리해서 사용자에게 이해할 수 있는 응답을 보내는 걸 말한다.


Spring MVC의 예외 처리

Spring MVC에서는 컨트롤러에서 예외가 터졌을 때 자동으로 예쁘게 처리할 수 있게 도와주는 여러 기능을 제공하는데, 대표적으로

  • @ExceptionHandler
  • @ResponseStatus
  • @ControllerAdvice

1. @ExceptionHandler

@ExceptionHandler(MyCustomException.class)
public ResponseEntity<ErrorResponse> handleMyException(MyCustomException ex) {
    return ResponseEntity
        .status(HttpStatus.BAD_REQUEST)
        .body(new ErrorResponse("CODE", ex.getMessage()));
}
  • 특정 예외가 발생하면 이 메서드가 호출
  • 직접 상태 코드, 응답 메시지 등을 설정 가능
  • 컨트롤러 클래스 안에 만들거나, 전체 공통 처리를 위해 @ControllerAdvice와 같이 사용

2. @ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
    public NotFoundException(String message) {
        super(message);
    }
}
  • 예외 클래스 위에 붙여서 이 예외가 발생하면 어떤 HTTP 상태코드를 보낼지 지정 가능.
  • 예를 들어, NotFoundException이 발생하면 자동으로 404 상태코드가 응답된다.
  • @ResponseStatus는 단순하고 간편하지만, 응답 body를 꾸밀 수 없다.

3. @ControllerAdvice

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity<ErrorResponse> handleCustom(MyCustomException ex) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse("CODE", ex.getMessage()));
    }
}
  • 프로젝트 전체(모든 컨트롤러)에서 발생하는 예외를 한 곳에서 처리 가능

예외 처리 흐름 요약

  1. 예외 발생 (throw new SomeException())
  2. → 해당 예외를 처리할 @ExceptionHandler 또는 @ResponseStatus 있는지 찾음
  3. → 있으면 해당 핸들러 실행
  4. → 없으면 Spring이 기본 500 에러 응답

Spring Security의 예외 처리

Spring Security는 요청이 DispatcherServlet까지 도달하기 전에 먼저 필터 체인에서 요청을 가로채서 인증(authentication) 또는 인가(authorization) 를 검사한다.

이 단계에서 예외가 발생하면, Spring MVC의 컨트롤러에 도달하지 않았기 때문에, @ExceptionHandler나 @ControllerAdvice가 예외를 잡을 수 없다.

그래서 Spring Security는 자체적으로 예외 핸들러를 제공하는데, 그게 바로 아래 두 가지

  1. AuthenticationEntryPoint
  2. AccessDeniedHandler

1. AuthenticationEntryPoint

👉 인증(로그인)되지 않은 사용자가 보호된 URL에 접근하려 할 때 동작

예: JWT 토큰이 없거나 유효하지 않은 경우

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                     AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
        response.setContentType("application/json");
        response.getWriter().write("{\\"code\\":\\"AUTH_REQUIRED\\",\\"message\\":\\"로그인이 필요합니다.\\"}");
    }
}

2. AccessDeniedHandler

👉 인증은 됐지만, 해당 리소스에 접근 권한이 없는 경우 동작

예: 일반 유저가 관리자 페이지에 접근 시도

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
        response.setContentType("application/json");
        response.getWriter().write("{\\"code\\":\\"ACCESS_DENIED\\",\\"message\\":\\"접근 권한이 없습니다.\\"}");
    }
}

 

이 두 개를 Security 설정에 등록해야 적용된다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().permitAll()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeniedHandler())
            )
            .build();
    }
}

Spring Security 주요 필터 7선

필터 이름  이름 실무 사용 여부
1. SecurityContextPersistenceFilter 이전 요청에서 저장된 인증 정보를 SecurityContext에 복원 ✅ 항상 사용됨
2. UsernamePasswordAuthenticationFilter username과 password로 로그인 처리 ❌ JWT 쓰면 사용 안 함
3. BasicAuthenticationFilter HTTP Basic 인증 (Authorization: Basic ...) 처리 ❌ API에서 거의 안 씀
4. BearerTokenAuthenticationFilter Spring에서 제공하는 Bearer Token 인증 필터 ⚠️ Spring Security 6+에서 가능
5. ExceptionTranslationFilter 예외 발생 시, 적절한 응답으로 바꿔줌 (401, 403) ✅ 중요
6. FilterSecurityInterceptor 최종적으로 권한 체크를 수행 (인가/Authorization) ✅ 항상 사용됨
7. CorsFilter CORS(Cross-Origin Resource Sharing) 설정 처리 ✅ 프론트엔드 연동 시 필수

1️⃣ SecurityContextPersistenceFilter

  • 매 요청마다 인증 정보를 담고 있는 SecurityContext를 세션에서 꺼내와 저장해 줌
  • JWT 기반에서는 stateless 처리를 위해 SessionCreationPolicy.STATELESS 설정하고 사용 안 하게 되기도 함

2️⃣ UsernamePasswordAuthenticationFilter

  • 사용자가 /login으로 POST할 때 아이디/비번을 읽어 인증 처리
  • JWT 쓰면 거의 비활성화 (formLogin().disable())

3️⃣ BasicAuthenticationFilter

  • Authorization: Basic abcdef== 형태의 요청 헤더를 해석
  • 간단한 인증엔 쓰이지만, 실무 API에선 거의 안 씀

4️⃣ ExceptionTranslationFilter

  • 인증/인가 도중 예외가 발생하면, 여기서 적절한 예외 응답으로 변환
  • 예: AuthenticationEntryPoint, AccessDeniedHandler 호출
  • 실질적으로 "401/403 응답을 반환하는 역할"을 함
  • FilterSecurityInterceptor에서 발생한 예외는 ExceptionTranslationFilter가 감싸고 있다가 응답으로 바꿔서 보냄

5️⃣ FilterSecurityInterceptor

  • 마지막 단계에서 접근 제어(인가)를 실제로 수행
  • @PreAuthorize, hasRole(), hasAuthority() 등이 여기서 처리됨

6️⃣ CorsFilter

  • 다른 도메인(예: localhost:3000 → localhost:8080)에서 요청이 오면 허용할지 검사
  • 프론트엔드와 통신하는 백엔드라면 필수

 

요청 처리 흐름 예시 (JWT 기반)

[요청 도착]
   ↓
CorsFilter
   ↓
SecurityContextPersistenceFilter
   ↓
JwtAuthenticationFilter (addFilterBefore로 삽입)
   ↓
ExceptionTranslationFilter
   ↓
FilterSecurityInterceptor
   ↓
Controller 도달 (인증 완료된 상태)

 

끝.