9. API 예외 처리

2024. 5. 6. 20:35·Spring/MVC 2

HTML 페이지와 다르게, API의 경우는 각 오류 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 데이터를 보내주어야 한다.


이 장에서 배운 Response Http Body에 JSON으로 오류 데이터를 반환하는 방법은 2가지가 있다. (나머지는 View, ModelView 반환)

 

1) BasicErrorController : ResponseEntity

- view를 기준으로 JSON으로 반환을 한다.

- ResponseEntity에 에러 메시지를 넣어주는 것이 불편하다.

 

2) HandlerExceptionResolver : response.getWriter().println("...")

- 반환 타입인 ModelAndView는 Api 응답에는 필요하지 않다.

- 동일한 (Runtime)Exception 예외를 서로 다른 방식으로 처리하기 어렵다.

- Response에 직접 응답 데이터를 넣어주는 것이 불편하다.

 

 

하지만 다음 장에 배울 @ExceptionHandler의 편의성이 막강해 이를 사용하면 좋다.

 

 


 

1. API 예외 처리 - 직접 (HashMap)

클라이언트가 정상∙오류 요청 상관없이 반환값이 JSON이기를 기대한다면 ResponseEntity<>()를 사용할 수 있다.

 

ResponseEntity<>()
스프링이 ResponseEntity의 body로 지정한 객체를 사용해 JSON으로 변환한다.

이 때 추가적으로 헤더 정보와 HTTP 상태 코드를 함께 전달할 수 있다.

// 클라이언트가 받고 싶은 타입, 즉 Accept 헤더가 JSON_VALUE 일 때 이 메소드가 호출된다.
@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("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));
}

 

 


 

2. Error View 제공 → 기본 오류 처리 (BasicErrorController)

 

[BasicErrorController]

- Spring MVC에서 제공하는 기본 에러 컨트롤러

- '/error' 경로를 처리하는 두 개의 메소드가 있다.

     1. errorHtml() : 클라이언트 요청의 Accept 헤더 값이 text/html 인 경우 호출되어 view를 반환한다. (정의한 뷰 or 기본 뷰)

     2. error() : 그 외에 호출되어 ResponseEntity로 Http Body에 JSON 데이터를 반환한다. → @ExceptionHandler 사용

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}


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

 

 


 

3. Error JSON 제공 → API 예외 처리 (HandlerExceptionResolver)

→ 결국 ModelView() 반환 엔딩..

 

WAS까지 예외가 전달되면, HTTP 상태코드는 무조건 500으로 처리된다.

사용자 입력 오류와 같이 클라이언트 예외는 다른 상태코드로 처리할 수 있다.

또한 제공하는 API마다 오류 메시지, 형식 등을 다르게 처리할 수 있다.

 

 

1) HandlerExceptionResolver

컨트롤러에서 발생한 exception을 먹고,

응답에 ①sendError, ②response.getWriter().println() 를 포함하여 WAS에 정상 응답을 보낸다.

public interface HandlerExceptionResolver {
    // handler : 핸들러(컨트롤러) 정보
    // Exception ex : 핸들러(컨트롤러)에서 발생한 예외
    @Nullable
    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

 

 

 

2) ExceptionResolver



ExceptionResolver 적용 전

- WAS까지 예외가 전달이 되고, WAS에서 '/error' 경로로 내부 요청을 하여 에러 뷰를 띄운다.

 

ExceptionResolver 적용 후

- 컨트롤러에서 예외가 발생하면 ExceptionResolver가 해당 예외를 해결할 수 있으면(...) 해결을 한다.

   해결을 하면서 ModelAndView를 반환을 하고, WAS에는 정상 응답(그러나 'response.sendError()' 포함)을 전달한다.

   (WAS에서 respons에 포함된 error를 확인하면서 여전히 '/error' 내부 요청이 있다.)

   (예외를 해결해도 postHandle()은 호출되지 않는다.)

 

- 'response.getWriter().println("...")' 으로 response body에 직접 데이터를 넣어줄 수 있다.

   (response.sendError()가 없이, WAS에서 '/error' 내부 요청 없이 바로 JSON으로 오류를 반환할 수 있다.)

   (그러나 다음 장에 배울 '@ExceptionHandler' 를 사용하는 것이 좋다.)

 

 

 

 

3) MyHandlerExceptionResolver

[return 값에 따른 동작 방식]

 

1. 빈 ModelAndView

   : View를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.

 

2. ModelAndView 지정

   : View를 렌더링 한다.

 

3. null

   : 다음 ExceptionResolver를 찾아서 실행한다.

 

@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);
            e.printStackTrace();
        }
        return null;
    }
}
// ExceptionResolver() 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new MyHandlerExceptionResolver());
    }
}

 

 


 

4. Spring 기본 제공 → ExceptionResolver(1)

1) 우선순위

1. ExceptionHandlerExceptionResolver

   : @ExceptionHandler 처리

 

2. ResponseStatusExceptionResolver

   : HTTP 상태 코드 지정

 

3. DefaultHandlerExceptionResolver

 

   : 스프링 내부 기본 예외 처리 (ex. TypeMismatch)

 

 

 

2) ResponseStatusExceptionResolver

1. @ResponseStatus 예외

: 컨트롤러 내에서 발생한 예외가 컨트롤러 밖으로 넘어가면, ResponseStatusExceptionResolver가 @ResponseStatus을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST(400)으로 변경하고, 메시지를 담는다.

 

- sendError(statusCode, reason)으로 보낸다. → '/error' 내부 요청

- reason을 메시지화 가능하다.

- 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.

   (ex. 라이브러리 예외 코드)

GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {               
	throw new BadRequestException();
}
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}

 

 

 

2. ResponseStatusException 예외

 

- 조건에 따라 예외 동적으로 변경 가능

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

 

 

 

 

3) DefaultHandlerExceptionResolver

// 파라미터를 아무것도 넣지 않았다.
// http://localhost:8080/api/default-handler-ex

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

DefaultHandlerExceptionResolver가 500 error를 400 error로 응답 코드를 변경해준다.
DefaultHandlerExceptionResolver 동작 로그

 

 


 

4. Spring 기본 제공 → ExceptionResolver(2)

*ExceptionHandlerExceptionResolver

   : @ExceptionHandler 처리

 

@ExceptionHandler 어노테이션으로, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다.

그러면 해당 컨트롤러에서 예외가 발생하면 @ExceptionHandler 메서드가 호출된다.

(예외를 지정하지 않으면 메서드 파라미터 예외를 사용한다.)

 

 

 

1) 실행흐름

1. 컨트롤러에서 IllegalArgumentException 예외가 발생해, 컨트롤러 밖으로 던져진다.

 

2. 예외가 발생했으므로 ExceptionResolver가 작동한다.

     (그 중 가장 우선순위가 높은 'ExceptionHandlerExceptionResolver' 실행)

 

3. IllegalArgumentException 를 처리할 수 있는 @ExceptionHandler이 있는지 확인한다.

 

4. ExControllerAdvice에서 illegalExeptionHandler()를 실행한다.

     (@RestControllerAdvice이므로, @ResponseBody가 적용되어 HTTP 컨버터가 JSON으로 변환하여 반환된다.)

 

5. ResponseStatus() 상태 코드에 맞춰 응답한다.

 

 

 

2) @ControllerAdvice

특정 컨트롤러에서 발생할 수 있는 예외를 잡아 처리하는 기능을 한다.

(대상을 지정하지 않으면, 글로벌 적용한다.)

 

대상 컨트롤러 지정

// 공식문서 : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann- controller-advice

// @RestController 어노테이션이 붙은 모든 컨트롤러
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// "org.example.controllers" 경로 하위에 있는 모든 컨트롤러
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// "ControllerInterface.class", "AbstractController.class" 구체적인 컨트롤러
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

 

 

 

[ExControllerAdvice]

@Slf4j
@RestControllerAdvice(assignableTypes = {ApiExceptionV2Controller.class})
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    // @ResponseStatus를 지정해주지 않으면 200 OK으로 나간다.
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExceptionHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage()); // 그대로 정상 응답, JSON으로 반환된다.
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExceptionHandler(UserException e) {
        log.error("[exceptionHandler] 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 exceptionHandler(Exception e) {
        log.error("[exceptionHandler] ex ", e);
        return new ErrorResult("EX", "내부 오류");
    }

}
저작자표시

'Spring > MVC 2' 카테고리의 다른 글

11. 파일 업로드  (0) 2024.05.07
10. 스프링 타입 컨버터  (0) 2024.05.06
8. 예외 처리와 오류 페이지  (0) 2024.05.05
7. 로그인 처리2 - 필터, 인터셉터  (0) 2024.05.04
6. 로그인 처리1 - 쿠키, 세션  (1) 2024.05.02
'Spring/MVC 2' 카테고리의 다른 글
  • 11. 파일 업로드
  • 10. 스프링 타입 컨버터
  • 8. 예외 처리와 오류 페이지
  • 7. 로그인 처리2 - 필터, 인터셉터
wch_t
wch_t
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (168)
      • Architecture (0)
      • Algorithm (67)
        • Math (5)
        • Simulation (1)
        • Data Structure (4)
        • DP (7)
        • Brute Fource (10)
        • Binary Search (6)
        • Greedy (2)
        • Graph (11)
        • Mst (1)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (2)
        • Mitm (1)
      • CS (2)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (6)
      • DevOps (15)
        • AWS (9)
        • Docker (1)
        • CI-CD (5)
      • Error (1)
      • Project (0)
        • kotrip (0)
      • Spring (59)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (8)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • Test (2)
      • TIL (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Sxssf
    docker: not found
    docker
    aws secrets manager
    애플
    spring-cloud-starter-bootstrap
    response_mode
    백준 3015 파이썬
    Jenkins
    TempTable
    spring-cloud-starter-aws-secrets-manager-config
    view algorithm
    백준 17299 파이썬
    백준 17289 파이썬
    form_post
    apache poi
    scope
    Merge
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
9. API 예외 처리
상단으로

티스토리툴바