Java/SpringBoot

API 예외 처리

Choi G.H 2022. 8. 18. 17:24
반응형

기존 방식의 예외처리로는 API 요청 시

예외가 발생하지 않을 경우- 데이터가 정상적으로 반환되지만

예외가 발생하면- 오류 페이지를 반환한다.

 

그래서 오류 발생 시에도 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("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));
}

서블릿에서 status, message를 받아서 HashMap으로 반환한다.

 

스프링부트 기본 오류 처리

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

클라이언트 http request의 Accept헤더가:

- text/html 이면 errorHtml()이 호출되어 html 반환

- 그 외의 경우 error() 이 호출되어 ResponseEntity 반환

 

HandlerExceptionResolver

발생하는 예외에 따라서 다른 상태코드로 처리하고, 오류 메시지와 형식을 API별로 커스터마이징 하고싶다.

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

pathvariable에 따라 다른 예외를 throw 하도록 만들었다. 이제 bad를 파라미터로 넣어주면 500 에러가 뜬다.

 

IllegalArgumentException은 잘못된 요청으로 인한 에런데 서버에서 처리하지 못해서 500으로 넘어갔다.

 

이 경우 예외를 해결하고 동작을 처리할 방법이 필요하다.

 

실행 시점

1. preHandle(): 핸들러 이전

2. postHandle(): 핸들러 이후

3. aferCompletion(): 모든 처리가 끝난 이후

 

controller에서 예외가 전달되면 ExceptionResolver에서 ModelAndView를 반환한다.

 

ExceptionResolver는 WebConfigurer를 통해 등록한다.

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

 

 

SpringBoot가 제공하는 ExceptionResolver

 

1. ExceptionHandlerExceptionResolver

2. ResponseStatusExceptionResolver

3. DefaultHandlerExceptionResolver

 

의 순서로 등록된다.

 

ExceptionHandlerExceptionResolver: @ExceptionHandler 처리. API 예외 대부분 처리 가능.

ResponseStatusExceptionResolver: Http 상태코드 지정

DefaultHandlerExceptionResolver: 스프링 내부 기본 예외 처리

 

1. ResponseStatusExceptionResolver

package hello.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}

 

@ResponseStatus 애노테이션으로 Http상태코드를 변경해준다.

response.sendError(statusCode, resolvedReason)을 호출한다.

sendError를 호출해 WAS에서 다시 오류페이지를 호출한다.

 

package hello.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}

reason을 MessageSource에서 찾는 메시지 기능을 사용할 수 있다.

 

이런 @ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 사용할 수 없다. 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하기도 어렵다. 그럴때는 ResponseStatusException 예외를 사용한다.

 

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

 

2. DefaultHandlerExceptionResolver

스프링 내부 예외를 해결한다.

 

예) 파라미터 바인딩 시 타입이 맞지 않으면 TypeMismatchException이라는 내부 예외 발생

서블릿 컨테이너까지 예외가 올라가고 500 오류 발생

-> 클라이언트의 입력 오류에 의한 예외이므로 400 에러가 맞다.

 

DefaultHandlerExceptionResolver.handleTypeMismatch 의 코드를 보면 

response.sendError(HttpServletResponse.SC_BAD_REQUEST) 를 확인할 수 있다.

-->sendError를 통해 문제를 해결함

 

HTML 화면 오류 VS API 오류

 

API는 시스템마다 응답의 모양, 스펙 등이 모두 다르다. 같은 예외라도 어느 컨트롤러에서 발생했느냐에 따라 다르게 내려줘야 할 수도 있다. 

 

@ExceptionHandler

 

package hello.exception.exhandler;
import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionV2Controller {
 @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 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;
 }
}
@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.

 

@ControllerAdvice

대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여한다.

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
public class ExampleAdvice3 {}

 

반응형