GlobalExceptionHandler vs Security Handler: ResponseEntity 동작 차이

Sang_WJ·2025년 12월 22일

Spring

목록 보기
3/3
post-thumbnail

Spring MVC와 Filter 레벨에서의 예외 처리 방식 비교

목차


핵심 질문

Q: GlobalExceptionHandler에서는 ResponseEntity만 반환해도 HTTP 상태 코드가 설정되는데, Security Handler에서는 왜 response.setStatus()를 직접 호출해야 하나요?


결론부터 말하면

GlobalExceptionHandler (@RestControllerAdvice)

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
    return ResponseEntity.status(500).body(...);  // ✅ 이것만으로 충분
}

Spring MVC가 ResponseEntity를 자동으로 HTTP 응답으로 변환해줍니다.

Security Handler (CustomAuthenticationEntryPoint)

@Override
public void commence(..., HttpServletResponse response, ...) {
    response.setStatus(401);  // ❌ 직접 설정 필요
    response.setContentType("application/json");
    response.getWriter().write(json);  // ❌ 직접 작성 필요
}

Spring MVC 영역 밖이므로 직접 HttpServletResponse를 조작해야 합니다.


GlobalExceptionHandler의 동작 원리

위치

  • Spring MVC 컨텍스트 내부
  • @RestControllerAdvice 어노테이션으로 Spring Bean으로 관리됨

동작 과정

  1. Controller에서 예외가 발생
  2. DispatcherServlet이 예외를 가로챔
  3. @ExceptionHandler가 붙은 메서드 실행
  4. ResponseEntity 반환
  5. Spring이 자동으로 처리:
    • ResponseEntity.status(500) → HTTP 상태 코드 500 설정
    • .body(...) → MessageConverter로 JSON 직렬화
    • Content-Type: application/json 자동 설정
    • 응답 본문에 JSON 작성

코드 예시

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleUserNotFound(UserNotFoundException e) {
        // Spring이 아래 작업을 자동으로 수행:
        // 1. HTTP 상태 코드 404 설정
        // 2. ApiResponse를 JSON으로 변환
        // 3. Content-Type 헤더 설정
        // 4. 응답 본문 작성
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ApiResponse.error("USER-001", "사용자를 찾을 수 없습니다."));
    }
}

Security Handler의 동작 원리

위치

  • Servlet Filter Chain 레벨 (Spring MVC 이전)
  • DispatcherServlet이 실행되기 에 동작

동작 과정

  1. HTTP 요청 도착
  2. Filter Chain 실행 (Spring Security Filters)
  3. 인증/인가 실패 발생
  4. AuthenticationEntryPoint 또는 AccessDeniedHandler 호출
  5. Spring MVC가 아직 시작 안 됨 ⚠️
  6. 개발자가 직접 HttpServletResponse 조작 필요

코드 예시

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException authException)
            throws IOException, ServletException {

        // 1. HTTP 상태 코드 직접 설정
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  // 401

        // 2. Content-Type 직접 설정
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        // 3. JSON 직렬화 직접 수행
        ObjectMapper mapper = new ObjectMapper();
        ApiResponse<Void> errorResponse = ApiResponse.error("AUTH-001", "인증이 필요합니다.");
        String json = mapper.writeValueAsString(errorResponse);

        // 4. 응답 본문 직접 작성
        response.getWriter().write(json);
    }
}

잘못된 예시 ❌

// 이렇게 하면 작동하지 않습니다!
public void commence(..., HttpServletResponse response, ...) {
    ResponseEntity<ApiResponse<Void>> errorResponse =
        ResponseEntity.status(401).body(ApiResponse.error(...));

    String json = mapper.writeValueAsString(errorResponse);
    response.getWriter().write(json);
    // ❌ HTTP 상태 코드는 200 OK로 나감!
    // ❌ ResponseEntity 객체 전체가 JSON으로 직렬화됨!
}

요청 처리 전체 흐름

┌─────────────────────────────────────────────────────────────────┐
│                        클라이언트 요청                             │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  [1] Filter Chain (Servlet Filter 레벨)                         │
│      - JwtAuthenticationFilter                                  │
│      - Spring Security Filters                                  │
│      - CORS Filter, Encoding Filter 등                          │
│                                                                  │
│  ⚠️  인증/인가 실패 시                                            │
│      → CustomAuthenticationEntryPoint (401)                     │
│      → CustomAccessDeniedHandler (403)                          │
│                                                                  │
│  ⚡ 여기는 Spring MVC 밖!                                         │
│     HttpServletResponse 직접 조작 필요                           │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  [2] DispatcherServlet (Spring MVC 영역 시작)                   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  [3] HandlerMapping → Controller 실행                            │
│                                                                  │
│      예외 발생 시 → GlobalExceptionHandler                       │
│                                                                  │
│  ✅ 여기는 Spring MVC 안!                                         │
│     ResponseEntity 반환만 하면 Spring이 처리                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  [4] MessageConverter (JSON 변환)                                │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        클라이언트 응답                             │
└─────────────────────────────────────────────────────────────────┘

왜 이렇게 다를까?

Spring MVC의 책임 범위

GlobalExceptionHandler가 동작하는 영역

  • DispatcherServlet 내부
  • Spring MVC가 관리하는 Bean
  • Controller 실행 중 발생한 예외 처리
  • Spring의 MessageConverter 사용 가능
  • ResponseEntity를 자동으로 HTTP 응답으로 변환

Security Handler가 동작하는 영역

  • DispatcherServlet 이전 (Servlet Filter)
  • Spring MVC가 시작되기 전
  • Controller에 도달하기 전 인증/인가 처리
  • MessageConverter 사용 불가
  • HttpServletResponse 직접 조작 필요

비유로 이해하기

GlobalExceptionHandler: 식당 주방(Spring MVC) 안에서 일하는 요리사

  • 주방 도구(MessageConverter, ResponseEntity) 모두 사용 가능
  • 매니저(Spring)가 서빙까지 알아서 처리해줌

Security Handler: 식당 입구 경비원(Filter)

  • 주방 밖에서 일함
  • 주방 도구 사용 불가
  • 손님을 직접 응대하고 직접 문서(HTTP Response) 작성해야 함

코드 비교

GlobalExceptionHandler (Spring MVC 영역)

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
        // Spring이 자동으로 처리하는 것들:
        // ✅ HTTP 상태 코드 설정 (500)
        // ✅ Content-Type: application/json 설정
        // ✅ ApiResponse 객체를 JSON으로 직렬화
        // ✅ 응답 본문에 JSON 작성

        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error("SYS-001", "서버 오류가 발생했습니다."));
    }
}

실제 HTTP 응답:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "success": false,
  "error": {
    "code": "SYS-001",
    "message": "서버 오류가 발생했습니다."
  },
  "data": null
}

Security Handler (Filter 영역)

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException authException)
            throws IOException {

        // 모든 것을 직접 처리해야 함:

        // 1️⃣ HTTP 상태 코드 직접 설정
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  // 401

        // 2️⃣ Content-Type 직접 설정
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        // 3️⃣ JSON 직렬화 직접 수행
        ObjectMapper mapper = new ObjectMapper();
        ApiResponse<Void> errorResponse =
            ApiResponse.error("AUTH-001", "인증이 필요합니다.");
        String json = mapper.writeValueAsString(errorResponse);

        // 4️⃣ 응답 본문 직접 작성
        response.getWriter().write(json);
    }
}

실제 HTTP 응답:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=UTF-8

{
  "success": false,
  "error": {
    "code": "AUTH-001",
    "message": "인증이 필요합니다."
  },
  "data": null
}

정리 테이블

비교 항목GlobalExceptionHandlerSecurity Handler
실행 위치Spring MVC 내부 (DispatcherServlet 이후)Servlet Filter (DispatcherServlet 이전)
실행 시점Controller 실행 중Controller 실행 전 (인증/인가 단계)
Spring 관리✅ Spring Bean (@RestControllerAdvice)⚠️ Filter (부분적으로 관리)
ResponseEntity 사용✅ 자동으로 HTTP 응답으로 변환됨❌ 아무도 처리하지 않음
MessageConverter 사용✅ 자동으로 JSON 변환❌ 직접 ObjectMapper 사용 필요
HTTP 상태 코드 설정ResponseEntity.status() 자동 반영response.setStatus() 직접 호출
Content-Type 설정✅ 자동 설정response.setContentType() 직접 호출
응답 본문 작성✅ 자동 작성response.getWriter().write() 직접 호출
코드 복잡도🟢 간단 (Spring이 대부분 처리)🟡 복잡 (모든 것을 직접 처리)
사용 예시Controller 예외 처리, 비즈니스 로직 오류인증 실패(401), 권한 부족(403)

핵심 기억 포인트

✅ GlobalExceptionHandler

  • Spring MVC 영역 = Spring이 관리
  • ResponseEntity 반환만 하면 끝
  • Spring이 나머지를 자동으로 처리

⚠️ Security Handler

  • Filter 영역 = Spring MVC 밖
  • HttpServletResponse를 직접 조작해야 함
  • HTTP 상태 코드, Content-Type, JSON 직렬화, 응답 작성 모두 직접

참고 자료

profile
성장의 흔적을 남기는 블로그입니다.

0개의 댓글