Spring

[Spring] 서블릿 예외 처리 , 스프링 부트 예외 처리

개바리여리 2025. 6. 28. 16:53

클라이언트 요청 처리중 예외 발생시 사용자에게 의미 있는 에러 페이지 or 메세지를 반환하도록 서블릿 컨테이너에서 처리하는 방식이다.

 

서블릿 예외 처리는 WAS가 요청/응답 수준에서 예외를 잡아주는 저수준 처리 방식

서블릿이 예외를 처리하는 경우는 HttpServletResponse 의 sendError 메서드를 호출한 경우와 아닌 경우로 나뉘게 된다.

 

1. 호출하지 않고 예외가 그대로 전파된 경우

 

WAS는 만들어둔 예외 페이지가 있다면 해당 페이지를 보여주고 없으면 톰캣 기본 에러 페이지를 보여주게 된다.

 

 

2. sendError 호출한 경우

 

sendError()는 서블릿에서 WAS 의도적으로 에러 응답을 보낼 때 사용하는 메서드

WAS에게 명시적으로 에러라고 알릴 수 있다.

( 예외가 발생하지 않더라도 에러 응답을 줄 수 있음 )

 

if (userNotFound) {
    response.sendError(HttpServletResponse.SC_NOT_FOUND, "해당 유저를 찾을 수 없습니다");
    return;
}

 

sendError를 호출하면 응답 상태 코드가 정확히 설정된다.

response.sendError(404) → 실제 응답 헤더가 HTTP/1.1 404 Not Found로 세팅됨

 

//  브라우저는 정상 응답(200)으로 받아들임
response.setStatus(404);
response.getWriter().write("유저 없음");

// 브라우저와 클라이언트가 에러로 인식
response.sendError(404, "유저 없음");

 

컨트롤러가 아닌 Servlet이나 JSON응답이 아닌 HTML  에러 페이지 포워딩이 필요할때 sendError가 호출될 수 있다.

 

 

 

스프링부트 오류 처리

자바에서 try catch를 통하여 예외처리가 가능하지만 모든 코드에 붙히는건 비효율적이다.

스프링에서는 이러한 공통 관심사를 비지니스 로직에서 분리하는 다양한 예외 처리 방법을 제공한다.

전략패턴을 사용하여 예외처리를 추상화한 HandlerExceptionResolver 인터페이스를 만들었다.

 

이러한 예외응답은 WAS수준에서 처리하는 것이 아니기 때문에 WAS 입장에서는 정상 응답으로 간주된다.

 

스프링에서 오류 처리하는 방법에는 크게 4가지가 있다.

 

1. @ResponseStatus

 

에러 HTTP 상태를 변경하는 역할을 한다.

@ResponseStatus(value=HttpStatus.NOT_FOUND)
public class NotfoundException extends RuntimeException {

}

 

근데 이 방법은 WAS까지 예외를 전달시킨다. 이렇게 되면 커스터마이징같은 것이 설정과 관리가 귀찮아지기 때문에 

API 에러처리 방법으로 보통 @ExceptionHandler 가 많이 사용된다.

 

 

2. @ExceptionHandler

 

특정 예외 클래스에 대한 처리 메서드를 정의

 

@RestController
public class TestController {

    @GetMapping("/fail")
    public String fail() {
        throw new IllegalArgumentException("잘못된 요청입니다.");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegal(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("에러 메시지: " + ex.getMessage());
    }
}

 

해당 어노테이션은 Exception클래스들을 지정하여 예외를 지정 가능하다.

이러한 방법은 좀 더 유연하게 예외 처리가 가능해지는 장점이 있다.

 

해당 컨트롤러 내에서 발생한 예외에만 적용 가능하므로 컨트롤러에 에러처리 코드가 섞이며 난잡해질 수 있다.

그래서 전역적으로 관리하는 @RestControllerAdvie, @ControllerAdvice도 많이 사용된다.

 

 

 

3. @RestControllerAdvie, @ControllerAdvice

 

전역적으로 @ExceptionHandler를 적용 할 수 있는 어노테이션이다.

RestControllerAdvice는 REST API에서 json 형식으로 에러 응답을 관리할때 사용하는 것이고

ControllerAdvice는 여러 컨트롤러에서 공통으로 예외 처리할 때 사용하는 것이다.

 

@RestControllerAdvice // 또는 @ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegal(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("전역 처리: 잘못된 요청");
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAll(Exception ex) {
        return ResponseEntity.status(500).body("전역 처리: 서버 오류");
    }
}

 

public record ErrorResponse(String error, String message, LocalDateTime timestamp) {}

@RestControllerAdvice
public class GlobalHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handle(Exception ex) {
        return ResponseEntity.status(500).body(
            new ErrorResponse("INTERNAL_ERROR", ex.getMessage(), LocalDateTime.now())
        );
    }
}

 

@ControllerAdvice는 전역 예외 처리용이지만, 특정 컨트롤러만 지정해서 적용할 수도 있다.

@ControllerAdvice(basePackages = "com.example.controller.admin")
public class AdminExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAll(Exception ex) {
        return ResponseEntity.status(500).body("Admin 전용 처리");
    }
}

 

여러 ControllerAdvice가 있으면 @Order를 통하여 순서를 지정해줘야 일관된 응답을 해줄 수 있다.

그래서 되도록이면 하나의 ControllerAdvice에서 관리를 하고 여러 ControllerAdvice가 필요하다면

경로를 지정해주는것이 좋다.

 

 

Spring 예외 처리 Flow

사용자 요청
   ↓
서블릿 컨테이너 (Tomcat 등)
   ↓
DispatcherServlet
   ↓
핸들러 호출 (Controller, HandlerAdapter 등)
   ↓
예외 발생
   ↓
ExceptionResolver에게 전달 (핵심!)
   ↓
→ ① @ExceptionHandler
→ ② @ControllerAdvice
→ ③ ResponseEntityExceptionHandler
→ ④ 기본 ErrorController (기본 /error 처리)
   ↓
사용자에게 에러 응답 (HTML or JSON)

 

예외 발생 시 Spring 내부 처리 순서

  1. Controller나 그 내부에서 예외 발생
  2. DispatcherServlet이 예외를 감지
  3. 등록된 HandlerExceptionResolver들이 순서대로 처리 시도:
    • @ExceptionHandler (해당 컨트롤러 내부)
    • @ControllerAdvice (전역 처리기)
    • ResponseEntityExceptionHandler (스프링 기본 MVC 예외 처리기)
    • DefaultHandlerExceptionResolver (400, 405 등 내부 변환용)
    • 처리 안 되면 /error 경로로 이동 (기본 에러 처리)

예외처리기들은 스프링의 빈으로 등록되어 있고

 

예외가 발생하면 순차적으로 다음의 Resolver들이 처리가능한지 판별한 후에 예외가 처리된다.

 

1. ExceptionHandlerExceptionResolver

@ExceptionHandler가 선언된 컨트롤러 또는 @ControllerAdvice 클래스에서
예외를 처리할 수 있는 메서드를 찾아 실행한다.

  • 해당 예외를 처리할 수 있는 @ExceptionHandler 메서드를 검색하고 실행함.
  • 처리 가능한 핸들러가 없으면 다음 ExceptionResolver로 넘어감.

2. ResponseStatusExceptionResolver

예외 클래스 또는 예외 발생 시점에 @ResponseStatus 또는 ResponseStatusException이 적용되어 있으면,
지정된 HTTP 상태 코드를 기반으로 HttpServletResponse에 상태 코드와 메시지를 설정한다.

  • 상태 코드 설정은 response.sendError(...)로 이루어짐.
  • 이후 예외는 서블릿 컨테이너로 전달되어 /error 경로로 포워딩됨.
  • 최종적으로는 **BasicErrorController**가 /error 요청을 처리함.

3. DefaultHandlerExceptionResolver

스프링 내부에서 정의된 **표준 예외(예: HttpRequestMethodNotSupportedException 등)**를
자동으로 처리하여 적절한 HTTP 상태 코드로 응답한다.

  • @ExceptionHandler, @ResponseStatus 등으로 처리되지 않은 예외가 해당됨.
  • 예: 지원하지 않는 HTTP 메서드 → 405 Method Not Allowed,
    파라미터 타입 불일치 → 400 Bad Request 등.