
Spring MVC와 Filter 레벨에서의 예외 처리 방식 비교
Q: GlobalExceptionHandler에서는 ResponseEntity만 반환해도 HTTP 상태 코드가 설정되는데, Security Handler에서는 왜 response.setStatus()를 직접 호출해야 하나요?
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
return ResponseEntity.status(500).body(...); // ✅ 이것만으로 충분
}
Spring MVC가 ResponseEntity를 자동으로 HTTP 응답으로 변환해줍니다.
@Override
public void commence(..., HttpServletResponse response, ...) {
response.setStatus(401); // ❌ 직접 설정 필요
response.setContentType("application/json");
response.getWriter().write(json); // ❌ 직접 작성 필요
}
Spring MVC 영역 밖이므로 직접 HttpServletResponse를 조작해야 합니다.
@RestControllerAdvice 어노테이션으로 Spring Bean으로 관리됨@ExceptionHandler가 붙은 메서드 실행ResponseEntity 반환ResponseEntity.status(500) → HTTP 상태 코드 500 설정.body(...) → MessageConverter로 JSON 직렬화Content-Type: application/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", "사용자를 찾을 수 없습니다."));
}
}
AuthenticationEntryPoint 또는 AccessDeniedHandler 호출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 변환) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 클라이언트 응답 │
└─────────────────────────────────────────────────────────────────┘
GlobalExceptionHandler: 식당 주방(Spring MVC) 안에서 일하는 요리사
Security Handler: 식당 입구 경비원(Filter)
@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
}
@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
}
| 비교 항목 | GlobalExceptionHandler | Security 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) |
ResponseEntity 반환만 하면 끝HttpServletResponse를 직접 조작해야 함