본문 바로가기
Spring Study/MVC 패턴

[MVC 패턴] API 예외 처리

by 정재인 2023. 9. 4.

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

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}

@RequestMapping 
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}

- errorHtml(): produces = MediaType.TEXT_HTMl_VALUE: 클라이언트 요청의 Accept 헤더 값이 text/html인 경우에는 errorHtml()을 호출해서 view를 제공한다.

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

 

API 예외 처리 - HandlerExceptionResolver 

스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다.

 

 

HandlerExceptionResolver 인터페이스

public interface HandlerExceptionResolver {
    ModelAndView resolveException(
      HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex);
}

 

MyHandlerExceptionResolver

package hello.exception.resolver;
  import lombok.extern.slf4j.Slf4j;
  import org.springframework.web.servlet.HandlerExceptionResolver;
  import org.springframework.web.servlet.ModelAndView;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import java.io.IOException;
  @Slf4j
  public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@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;
      }
}

 

ExceptionResolver 활용

예외 상태 코드 변환

 예외를 response.sendError(x x x) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위함

 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어 스프링 부트가 기본으로 설정한 /error가 호출됨

뷰 템플릿 처리

 ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면을 뷰 렌더링해서 고객에게 제공

API 응답 처리

 response.getWriter().println("hello"); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하고 JSON으로 응답하면 API 응   답 처리를 할 수 있다.

 

HandlerExceptionResolver 활용

예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error를 호출하는 과정은 생각해보면 너무 복잡하다. ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 해결할 수 있다.

 


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

스프링 부트가 기본으로 제공하는 ExceptionReslover

1. ExceptionHandlerExceptionResolver

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

2. ResponseStatusExceptionResolver

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

3. DefaultHandlerExceptionResolver

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

 

API 예외 처리 - @ExceptionHandler

API 예외처리의 어려운 점

- HandlerExceptionResolver를 떠올려 보면 ModelAndView를 반환해야 했다. 이것은 API 응답에 필요하지 않다.

- API 응답을 위해 HttpServletResponse에 직접 응답 데이터를 넣어주었다. 이것은 과거 서블릿을 사용하던 시절로 돌아간 것 같다.

 

@ExceptionHandler 예외 처리 방법

@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.

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

 

다양한 예외 - 다양한 예외들을 한번에 처리할 수 있다.

@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
    log.info("exception e", e);
}

 

예외 생략 - 생략하면 메서드 파라미터의 예외가 지정된다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {}

 


API 예외 처리 - @ControllerAdvice

package hello.exception.exhandler;

import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class ApiExceptionV2Controller {
      
    @GetMapping("/api2/members/{id}")
    public 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 MemberDto(id, "hello " + id);
      }
      
      @Data
      @AllArgsConstructor
      static class MemberDto {
          private String memberId;
          private String name;
      }
}

 

@ControllerAdvice

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

- @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)

- @RestControllerAdvice는 @ControllerAdvice와 같고, @ResponseBody가 추가되어 있다.

 

 

 

 

 

댓글