빙응의 공부 블로그

[Spring]스프링 MVC 2편 - API 오류 처리 본문

Spring/인프런_개념

[Spring]스프링 MVC 2편 - API 오류 처리

빙응이 2024. 2. 6. 17:43

📝API 오류 처리 

API의 예외 처리는 다르게 처리해줘야 한다.

 

HTML 페이지의 경우 지금까지 설명했던 것 처럼 4XX,5XX 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있다.

그러나 API의 경우에는 생각할 내용이 더 많다. 오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만, API는 

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

 

  @GetMapping("/api/member/{id}")
  public MemberDto getMember(@PathVariable("id") String id){
    if(id.equals("ex")){
      throw  new RuntimeException("잘못된 사용자");
    }
    return new MemberDto(id, "hello"+ id);
  }
  @Data
  @AllArgsConstructor
  static class MemberDto{
    private String memberId;
    private String name;
  }

해당 코드로 PostMen을 사용해보자 

 

해당 사진처럼 오류 페이지 HTML이 넘어오는 것을 확인할 수 있다. 

그러나 API는 클라이언트에 정상이든 오류 요청이든 JSON으로 반환해야 한다. 

웹 브라우저가 아닌 이상 HTML을 직접 받아서 할 수 있는 것이 별로 없기 때문이다.

 

문제를 해결하려면 오류 페이지 컨트롤러도 JSON 응답을 할 수 있도록 수정해야 한다. 

 

API 응답 추가 
  @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Map<String,Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response){
    log.info("API errorPage 500");

    Map<String, Object> result = new HashMap<>();
    Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
    result.put("statue", 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));
  }

 

하나하나씩 살펴보자

  @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)

해당 코드는 JSON 매칭이 들어올 때 하는 것이다. 기본적으로 자바는 더 구체적인 것이 우선순위기 때문에 JSON일 때만 

해당 컨트롤러가 반응한다. 

    return  new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));

해당 리턴은 ResponseEntity를 정의하는 것으로 Map으로 JSON을 정의 HttpStatus.valueOf로 상태코드를 적은 것이다.

 

 

📝API 예외 처리 - 스프링 부트 기본 오류 처리

API도 스프링부트 기본 처리를 사용할 수 있다. 

 

일단 포스트맨을 사용해보자 

JSON 방식으로 보내면 아래 처럼 서블릿 컨테이너가 JSON을 자동 생성해준다.

{
    "timestamp": "2024-02-06T08:00:09.372+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/member/ex"
}

 

스프링 부트가 자동으로 처리해주는 BasicErrorController 코드를 보자

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponseresponse) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}

해당 처럼 HTML 텍스트의 경우 ModelAndView를 반환하여 자동으로 매핑해준다. ex) 500.html 4xx.html

만약 텍스트가 아닌 경우 ResponseEntity를 반환해준다. 우리가 방금한 방식과 같은 것이다. 

 

그러므로 BasicErrorController를 확장하면 우리가 API 오류 메시지를 원하는 것으로 만들 수 있다!

물론 API 오류 처리의 끝판왕 @ExceptionHandler를 사용하면 되므로 BasicErrorController를 확장해서 JSON 오류 메시지를 변경할 수 있다는 것만 알아두자.

 

API 오류는 기본 HTML 오류와 전혀 궤를 달리한다. 그 이유는 API마다 모두 오류 페이지를 다르게 해야할 가능성이 있기 때문이다. 

 

 

📝API 예외 처리 - HandlerExceptionResolver 

API의 오류를 발생 예외에 따라 다른 상태코드로 처리해보자 

상태코드 변환
예를 들어서 IllegalArgumentException을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면
HTTP 상태코드를 400으로 처리하고 싶다. 어떻게 해야할까?

 

🚩HandlerExceptionResolver 

스프링 MVC는 컨트롤러 밖으로 예외가 던져지니 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.

컨트롤러 밖을 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다.

 

ExceptionResolver는 컨트롤러에서 발생한 예외를 정상적으로 해결하는 해결사 역할이다.

 

HandlerExceptionResolver  인터페이스

public interface HandlerExceptionResolver {
 ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex);
}
  • handler : 핸들러 정보
  • Exception ex : 핸들러에서 발생한 예외 
  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    try {
      if (ex instanceof IllegalArgumentException) {
        log.info("IllegalArgumentException resolver to 400");
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        return new ModelAndView();
      }
    } catch (IOException e) {
      log.error("resolver ex", e);
    }
    return null;
  }

ModelAndView를 반환하는 이유는 정상적인 리턴으로 예외를 무효화 시키는 것이다. 

빈 객체를 반환하면 클라이언트에는 어떠한 응답도 주지 않기 때문이다.

 

여기서 IllegalArgumentException 이 발생하면 response.sendError(400)를 호출해서 HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView를 반환한다.

 

반환 값에 따른 동작 방식

  • 빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
  • ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더 링 한다
  • null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.

ExceptionResolver 활용 방식

  • 예외 상태 코드 변환
    • 예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류 처리를 하도록 위임
    • 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 error 호출
  • 뷰 템플릿 처리
    • ModelAndView 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 가능
  • API 응답 처리
    • response.getWriter().println("hello); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것이 가능 
    • JSON 으로 응답하면 API 응답 처리도 가능하다.

 

참고로 빈 WebConfig에 등록해줘야 정상 작동한다. 

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

 

📝API 예외 처리 - HandlerExceptionResolver 활용

예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지를 찾아 다시 /error 호출하는 과정은 너무 복잡하다.

ExceptionResolver를 활요하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다.

 

먼저 사용자 정의 예외를 추가해보자 

public class UserException extends RuntimeException {
 public UserException() {
 super();
 }
 public UserException(String message) {
 super(message);
 }
 public UserException(String message, Throwable cause) {
 super(message, cause);
 }
 public UserException(Throwable cause) {
 super(cause);
 }
 protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 super(message, cause, enableSuppression, writableStackTrace);
 }
}

 

UserException을 처리하는 ExceptionResolver를 만들어보자

JSON, HTML을 구분해야 한다.

  private  final ObjectMapper objectMapper = new ObjectMapper();
  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    try {
      if(ex instanceof UserException){
        log.info("UserException resolver to 400");
        String accepHeader = request.getHeader("accept");
        //response의 오류 상태 코드를 400으로 처리 
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
		
        //JSON일 경우 응답으로 오류 정보 JSON을 보낸다.
        if("application/json".equals(accepHeader)){
          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){
      log.error("resolver ex", e);
    }
    return null;
  }
  @Override
  public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
    resolvers.add(new UserHandlerExceptionResolver());
  }

 

이 코드에도 문제가 있다. 바로 구현이 귀찮은 것! (개발자는 귀찮은 것이 싫다)

 

 

📝API 예외 처리 - 스프링이 제공하는 ExceptionResolver 

스프링 부트가 기본으로 제공하는 ExceptionResolver를 사용하면 편리하게 구현이 가능하다.

 

HandlerExceptionResolverComposite 에 다음 순서로 등록한다

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver 우선 순위가 가장 낮다

ExceptionHandlerExceptionResolver

@ExceptionHandler을 처리한다. API 예외 처리 기능의 대부분을 이 기능으로 해결한다.

가장 중요한 기능으로 맨 마지막에 추가하겠다.

 

ResponseStatusExceptionResolver

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

예) @ResponseStatus(value = HttpStatus.NOT_FOUND)

 

DefaultHandlerExceptionResolver

스프링 내부 기본 예외를 처리한다.

 

각 HandlerExceptionResolverComposite들은 NULL을 반환하거나 처리하지 못하면 다음 순위를 처리하는 형식으로 동작한다. 

 

🚩ResponseStatusExceptionResolver

ResponseStatusExceptionResolver는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.

 

다음 두 가지 경우를 처리한다.

  • @ResponseStatus  예외
  • ResponseStatusException예외

 

예외에 다음과 같이 @ResponseStatus 애노테이션을 적용하면 HTTP 상태 코드를 변경해준다. 

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

BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.

 

ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason) 를 호출하는 것을 확인할 수 있다. sendError(400) 를 호출했기 때문에 WAS에서 다시 오류 페이지( /error )를 내부 요청한다.

 

 

또한 메시지 기능도 제공한다.

즉 messages.properties에 오류 메시지를 관리할 수 있다. 

//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}
messages.properties
error.bad=잘못된 요청 오류입니다. 메시지 사용

 

개발자 변경 예외 처리 

@ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.(애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용이 불가능하다)

추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경할 수 없다. 

이때 ResponseStatusException 예외를 사용하면 된다. 

 

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

 

해당 구문은 3번째 요소를 변환하는 것이다.

자세한 코드는 다음과 같다.

 

 

🚩 DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.

대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가 결과적으로 500 오류가 발생한다.

그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다. HTTP에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다. 

DefaultHandlerExceptionResolver 는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다

 

DefaultHandlerExceptionResolver.handleTypeMismatch의 코드는 결국

response.sendError(HttpServletResponse.SC_BAD_REQUEST)을 통해 HTTP 상태 코드를 400으로 만든다. 

 

예를 들어

@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
 return "ok";
}

이 코드 파라미터에 문자를 입력하면 400 오류가 생긴다. 그 이유는 DefaultHandlerExceptionResolver가   response.sendError(HttpServletResponse.SC_BAD_REQUEST)을 실행시켜 500에서 400으로 만들기 때문이다.

 

DefaultHandlerExceptionResolver는 무수한 타입의 상태코드에 대한 변환을 가지고 있다. 한번씩 확인해보도록 하자

 

✔정리

지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태코드를 변경하는 기능도 알아보았다.

그런데 HandlerExceptionResolver를 직접 사용하기는 복잡하다. API 오류 응답의 경우 response에 직접 데이터를 때려박아야 한다. ModelAndVieew를 사용하는 것도 API에 맞지 않는다. 그래서 스프링은 이 문제를 해결하기 위해 @ExceptionHandler라는 기능을 제공한다. 

 

 

📝 @ExceptionHandler

사실상 이번 포스팅의 목적으로 앞서 했던 API 오류 처리의 끝판왕이다. 

 

다시 한번 정리하자

 

HTML 화면 오류 vs API 오류

웹 브라웢는 HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController를 사용하면 된다.

이때는 단순히 5xx, 4xx 관련된 오류 화면을 보여주면 된다.

 

그런데 API는 각 시스템마다 응답의 모양도 다르고, 스펙도 모두 다르다. 예외 상황에서 오류 화면을 보여주는 것이 아니라

예외 상황에 다라 다른 데이터를 출력해줘야 한다. 매우 세밀한 제어가 필요한 것이다. 

 

지금까지 살펴본 BasicErrorController는 API에 맞지 않고 HandlerExceptionResolver는 직접 구현하기 쉽지 않다.

 

API 예외 처리의 어려운 점

  • HandlerExceptionResolver의 경우 ModelAndView를 반환했다. 이것은 API응답에 부적합하다.
  • API 응답을 위해 HttpServletResponse에 직접 응답 데이터를 넣어야 했다. 이것은 매우 불편하다. MVC에서 서블릿을 쓰는 것처럼 말이다.
  • 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다. 예를 들어 RuntimeException 예외에 대해 서로 다른 엔티티로 다른 방식처리가 필요하다면 어떻게 해야할까?
@ExceptionHandler
스프링은 API 예외 처리 문제를 해결하기 위해 이 기능을 제공한다.
실무에서도 API 예외 처리는 대부분 이 기능을 사용한다.

 

먼저 예제로 알아보자 

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

예외 발생 API 응답으로 사용할 객체 정의

 

 @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandler(IllegalArgumentException e){
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("BAD",e.getMessage());
  }

  @GetMapping("/api2/members/{id}")
  public ApiExceptionController.MemberDto getMember(@PathVariable("id") String id){
    if (id.equals("ex")) {
      throw new RuntimeException("잘못된 사용자");
    }
    if (id.equals("bad")) {
      throw new IllegalArgumentException("잘못된 입력 값");
    }
    if (id.equals("user-ex")) {
      throw new UserException("사용자 오류");
    }
    return new ApiExceptionController.MemberDto(id, "hello"+ id);
  }

위 코드는 IllegalArgumentException 오류 발생 시 해당 오류를 변경하는 것이다.

 

  @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandler(IllegalArgumentException e){
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("BAD",e.getMessage());
  }

@ExceptionHandler는 해당 컨트롤러에서 발생 오류를 지정하여 처리가 가능해진다.

또한 리턴에서 정상흐름으로 완전히 바꾸기 때문에 HTTP 상태 코드가 200이 된다.

만약 다른 코드로 바꾸고 싶으면 

 @ResponseStatus(HttpStatus.BAD_REQUEST)
 @ExceptionHandler(IllegalArgumentException.class)
 public ErrorResult illegalExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
 }

 위에 @ResponseStatus()를 넣어주면 된다.

 

 

참고로 ResponseEntity를 사용해도된다. 완전히 컨트롤러 형식으로 봐도 된다.

@ExceptionHandler
 public ResponseEntity<ErrorResult> userExHandle(UserException e) {
   log.error("[exceptionHandle] ex", e);
   ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
   return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
 }

 

 

전체적인 코드는 다음과 같다.

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
  }
  
  @ExceptionHandler
  public ResponseEntity<ErrorResult> userExHandle(UserException e) {
    log.error("[exceptionHandle] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler
  public ErrorResult exHandle(Exception e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("EX", "내부 오류");
  }

  @GetMapping("/api2/members/{id}")
  public ApiExceptionController.MemberDto getMember(@PathVariable("id") String id){
    if (id.equals("ex")) {
      throw new RuntimeException("잘못된 사용자");
    }
    if (id.equals("bad")) {
      throw new IllegalArgumentException("잘못된 입력 값");
    }
    if (id.equals("user-ex")) {
      throw new UserException("사용자 오류");
    }
    return new ApiExceptionController.MemberDto(id, "hello"+ id);
  }

여기서 exHandler는 최상위 오류로 위 두가지 세부사항 오류가 처리하지 못하면 exHandler가 호출된다. 

 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
 @ExceptionHandler
 public ErrorResult exHandle(Exception e) {
   log.error("[exceptionHandle] ex", e);
   return new ErrorResult("EX", "내부 오류");
 }

 

실행흐름

앞에서 배운 정보로 실행 흐름을 알아보자

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
 log.error("[exceptionHandle] ex", e);
 return new ErrorResult("BAD", e.getMessage());
}
  • 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
  • 예외가 발생햇으므로 ExceptionResolver가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.
  • ExceptionHandlerExceptionResolver는 해당 컨트롤러에  IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인한다. 
  • illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.
  • @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답 한다.

 

 

 

 

참고! ExceptionHandler는 다양한 파라미터와 응답을 지정할 수 있어 메뉴얼을 보는 것을 추천한다.

Exceptions :: Spring Framework

 

Exceptions :: Spring Framework

@Controller and @ControllerAdvice classes can have @ExceptionHandler methods to handle exceptions from controller methods, as the following example shows: @Controller public class SimpleController { // ... @ExceptionHandler public ResponseEntity handle(IOE

docs.spring.io

 

자 이제 문제가 뭔지 알겠죠?
모든 컨트롤러에 오류 처리 핸들러를 넣을 수 없잖아! 
너무 중복이야 

 

📝 @ControllerAdvice

@ExceptionHandler를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여있다. @ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있다.

 

 

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
  }
  @ExceptionHandler
  public ResponseEntity<ErrorResult> userExHandle(UserException e) {
    log.error("[exceptionHandle] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
  }
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler
  public ErrorResult exHandle(Exception e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("EX", "내부 오류");
  }
}

해당 코드처럼 @RestControllerAdvice를 통해 완전히 분리 가능하다.

 

@ControllerAdvice

  • @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능을 부여해준다.
  • 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.
  • 참고로 @RestControllerAdvice 는 @ControllerAdvice 와 같다. Rest형식인지 차이 

 

대상 컨트롤러 지정 방법

//타겟이 RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
//타겟의 경로 지정 가능 
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
//타겟을 직접 지정
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

더 자세한 것은 공식 문서를 참고하자

Controller Advice :: Spring Framework

 

Controller Advice :: Spring Framework

@ExceptionHandler, @InitBinder, and @ModelAttribute methods apply only to the @Controller class, or class hierarchy, in which they are declared. If, instead, they are declared in an @ControllerAdvice or @RestControllerAdvice class, then they apply to any c

docs.spring.io

 

 

✔정리

오늘은 API 오류 처리에 대해 알아봤다. 점점 발전하는 식으로 말이다.

대부분의 오류는 ExceptionHandler로 처리하지만 발전 과정 정도는 가볍게 알아보는 것이 좋다.

세세한 부분을 바꾸며 개발하는 것이기 때문에 나중에 오류나 커스텀이 필요하면 도움이 될 것이다.