💠프로그래밍 언어/Spring

[Spring] API 에러 처리하는 방법, ExceptionResolver 와 @ControllerAdvice

2025. 4. 6. 19:17
728x90

 

서블릿 API 에러 처리

에러 페이지는 단순히 에러 페이지만 랜더링 하면 되지만,

API 는 각 오류 상황에 맞는 응답 스펙을 정하고, 데이터를 JSON 으로 주어야 한다.

 

오류 페이지 컨트롤러에 JSON 응답을 해주는 메서드를 추가하면 된다.

@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(
                                   HttpServletRequest request, HttpServletResponse response) {

  Map<String, Object> result = new HashMap<>();
  Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
  result.put("status", request.getAttribute(ERROR_STATUS_CODE));
  result.put("message", ex.getMessage());
  
  Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
  return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
}

`produces = MediaType.APPLICATION_JSON_VALUE`

  : 클라이언트가 요청하는 HTTP Header 의 `Accept` 값이 `application/json` 일 때 해당 메서드가 호출

 

스프링 부트 API 에러 처리

BasicErrorContoller

`BasicErrorController` 코드에는 `errorHtml()`, `error()` 두 메서드가 있다.

- `errorHteml()` : produces = MediaType.TEXT_HTML_VALUE 를 통해 클라이언트 요청의 Accept 헤더 값이 text/heml 인 경우에 호출되어 view 를 제공한다.

- `error()` : 그 외 경우에 호출되며 ResponseEntity 로 HTTP Body 에 JSON 데이터를 반환한다.

 

따라서 스프링 부트는 `BasicErrorController` 가 제공하는 기본 정보를 활용하여 오류 API 를 자동으로 생성해 준다.

 

HandlerExceptionResolver 구현하여 만들기

예외가 발생하여 WAS 까지 예외가 전파되면 HTTP 상태코드가 500으로 처리된다.

 

이렇게 HandlerExceptionResolver 는 예외가 발생했을 때

상태코드, 에러 메시지, 형식 등 다양하게 처리할 수 있도록 도와준다.

 

동작 흐름

            preHandle
         ↗ 
   preHandle
 ↗ 
   
클라이언트 HTTP 요청
→ → → →
Dispatcher
Servlet
 handle(handler)
→ → → → → →
← ← ← ← ← ←
 예외 전달
핸들러
어댑터
③  예외 발생
→ → → →
핸들러
(컨트롤러)
 
 render(model)
호출
 ↘    예외 해결 시도
       ExceptionResolver
       ↘
           ⑦ afterCompletion
              afterCompletion
   
HTML 응답
← ← ← ←
View        

예외가 발생하면 ExceptionResolver 가 호출되어 예외 해결을 시도한다.

이때 예외가 해결되어도 postHandle( ) 은 호출되지 않는다.

 

사용 예시 - response.sendError(xxx)

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, 
                                       Object handler, Exception ex) {
                                       
    try {
      if (ex instanceof IllegalArgumentException) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        return new ModelAndView();
      }
    } catch (IOException e) {
    }
    
    return null;
  }
}

응답에 response.sendError(xxx) 를 통해 원하는 HTTP 상태코드와 메시지를 담아준다.

 

사용 예시 - response.setStatus(xxx)

public class UserHandlerExceptionResolver implements HandlerExceptionResolver {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, 
                                       Object handler, Exception ex) {
                                       
    try {
      if (ex instanceof IllegalArgumentException) {
        String acceptHeader = request.getHeader("accept");
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        
        if ("application/json".equals(acceptHeader)) {
          Map<String, Object> errorResult = new HashMap<>();
          errorResult.put("ex", ex.getClass());
          errorResult.put("message", ex.getMessage());
          String result = objectMapper.writeValueAsString(errorResult);
          
          response.setContentType("application/json");
          response.setCharacterEncoding("utf-8");
          response.getWriter().write(result);
          
          return new ModelAndView();
        } else {
          return new ModelAndView("error/400");
        }
      }
    } catch (IOException e) {
    }
    
    return null;
  }
}

응답에 response.setStatus 를 통해 원하는 HTTP 상태코드를 담아준다.

 

HTTP 요청 헤더의 ACCEPT 값이 application/json 이면 JSON 으로 오류를 전달하고,

그 외의 경우는 HTML 에러 페이지를 보여준다.

 

@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
  resolvers.add(new MyHandlerExceptionResolver());
  resolvers.add(new UserHandlerExceptionResolver());
}

`extendHandlerExceptionResolvers` 를 통해 ExceptionResolver 를 등록한다.

 

> 참고 <

등록할 때, configureHandlerExceptionResolvers( ) 를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 extendHandlerExceptionResolver 를 사용해야 한다.

 

반환값에 따른 동작 방식

`빈 ModelAndView` : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴

`ModelAndView 지정` : 지정한 뷰를 렌더링

`null` : 처리할 수 있는 다음 ExceptionResolver 를 찾고, 없으면 기존에 발생한 예외를 서블릿 밖으로 던짐

 

활용

- 예외 상태 코드 변환

  : response.sendError(xxx) 사용

    > 이후 WAS 는 내부적으로 BasicErrorController 를 통해 에러 페이지를 찾아 호출하여 추가 프로세스가 실행된다.

  : response.setStatus(xxx) 사용

    > 스프링 MVC 에서 예외 처리가 끝이 나고, WAS 에서는 정상 처리되어 추가 프로세스가 실행되지 않는다.

- 뷰 템플릿 처리

  : ModelAndView 를 지정하여 예외에 따른 새로운 에러 페이지 뷰 렌더링 가능

- API 응답 처리

  : response.getWriter( ).println("hello") 처럼 HTTP 응답 바디에 직접 JSON 등 데이터를 넣어줄 수도 있음

 

스프링 ExceptionResolver

스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음 순서로 등록되어 사용된다.

1. `ExceptionHandlerExceptionResolver`

2. `ResponseStatusExceptionResolver`

3. `DefaultHandlerExceptionResolver`

 

ExceptionHandlerExceptionResolver

@ExceptionHandler 를 처리한다.

 

`@ExceptionHandler`

API 오류 응답의 경우에 발생하는 아래의 문제점을 해결한다.

1. ModelAndView 로 반환

2. response 에 직접 응답 데이터를 넣기

3. 동일한 예외를 컨트롤러 별로 처리하기 어려움

 

사용 방법

@Data
@AllArgsConstructor
public class ErrorResult {
  private String code;
  private String message;
}

예외가 발생했을 때 API 응답으로 사용하는 객체를 정의한다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
  return new ErrorResult("BAD", e.getMessage());
}

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(Exception e) {
  ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
  return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}

@RestController 로, @ResponseBody 가 적용되어 HTTP 컨버터가 사용되어 응답이 JSON 으로 자동 변환된다.

 

`@ExceptionHandler`

  : 해당 컨트롤러에서 처리하고 싶은 예외를 지정

  : 예외가 발생하면 이 메서드가 호출되며, 지정한 예외 + 자식 클래스까지 잡을 수 있다.

  : @ExceptionHandler 에 예외를 생략하면, 메서드 파라미터의 예외가 지정된다. 

`@ResponseStatus` 

  : HTTP 상태 코드를 지정한다.

  : 애노테이션이므로 HTTP 상태 코드를 동적으로 변경할 수 없다.

`ResponseEntity`

  : HTTP 상태 코드와 메시지를 지정한다.

  : 프로그래밍해서 HTTP 상태 코드를 동적으로 변경할 수 있다.

 

ResponseStatusExceptionResolver

HTTP 상태 코드를 지정해 준다.

 

- `@ResponseStatus` 달려있는 예외

response.sendError(xxx) 로 HTTP 상태 코드를 변경하고, 메시지도 담는다.

이 때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}

reason 은 `MessageSource` 에서 찾는 기능도 제공한다.

(ex. `reason = "error.bad"`)

 

하지만 직접 변경할 수 없는 예외에는 적용할 수 없고, 조건에 따라 동적으로 변경하는 것이 어렵다.

 

- `ResponseStatusException` 예외

동적으로 HTTP 상태코드와, 에러 메시지, 실제 발생한 예외를 지정할 수 있다.

@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
  throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

이때도 `MessageSource` 에서 찾는 기능이 제공된다.

 

DefaultHandlerExceptionResolver

스프링 내부에서 발생하는 기본 예외를 처리한다.

 

예를 들어, 파라미터 바인딩 시점에 타입이 맞지 않으면 발생하는 `TypeMismatchException` 은

WAS 까지 전달되어 500 오류를 발생한다.

하지만 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로

DefaultHandlerExceptionResolver 가 400 오류로 변경한다.

 

이때, response.sendError(xxx) 를 사용했기 때문에 WAS 에서 내부적으로 에러 페이지를 호출한다.

 

@ControllerAdvice

@ControllerAdvice, @RestControllerAdvice 를 사용하여 @ExceptionHandler 와 컨트롤러 메서드를 분리할 수 있다.

 

@ControllerAdvice

- 대상으로 지정한 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해 주는 역할

- 대상을 지정하지 않으면 모든 컨트롤러에 적용

- @RestControllerAdvice 는 @ResponseBody 가 추가된 것과 같다.

 

대상 컨트롤러 지정 방법

@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 | 스프링 MVC 2(김영한) - 인프런

728x90