API 예외처리는 어떻게 해야할까?
API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다. -> 각 통신마다 쓰는 방식이 다다르기 때문에 이를 공통적으로 약속(오류 응답 스펙)을 하고 사용해야 한다.
서블릿 API 예외처리
기존의 예외처리는 클라이언트가 예외를 발생했을 때 HTML 형태의 뷰템플릿을 반환했었다. 하지만, 클라이언트에서 API로 요청을 보내면 서버에서도 API로 반환해주어야 하는데 별도의 설정 없이 진행하게 되면 서버는 똑같이 뷰템플릿을 반환한다.
API 예외처리 컨트롤러
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
//api에 'ex'라는 id가 들어오면 런타임 예외 발생.
if(id.equals("ex")){
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id,"hello " + id);
}
...
예외 발생 시, 서버에서의 처리 (WebSeverCustomizer)
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* 웹 서버를 커스터 마이징할 수 있는 방식
*/
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
//에러 페이지 생성 (스프링 부트 제공)
//WAS -> 컨트롤러로 설정한 경로를 호출하면서 해당 오류페이지를 호출한다.
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404"); // 400번 에러
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500"); // 500번 에러
//런타임 에러 페이지 생성 -> 런타임에러의 자식 예외 또한 다 적용된다.
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
//에러 페이지 등록
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
예외 페이지 출력 컨트롤러
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class ErrorPageController {
//RequestDispatcher 상수로 정의되어 있음
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";
public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
//404 예외 발생 시 해당 오류 페이지 출력.
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 404");
printErrorInfo(request);
return "error-page/404";
}
//500 예외 발생 시 해당 오류 페이지 출력.
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 500");
printErrorInfo(request);
return "error-page/500";
}
...
클라이언트 API 예외 요청

- 클라이언트에서 api호출을 하고 응답 또한 JSON으로 되기를 기대한다.
서버 응답

- 서버에서는 예외 발생 시, 해당 오류 상태코드에 따라서 뷰 템플릿을 출력하게끔 설정되어 있어서 JSON으로 응답하지 못한다.
- 따라서, 서버에서 JSON로 응답을 처리할 수 있도록 컨트롤러에서 별도의 처리 방식을 작성해야한다.
예외 발생 시, JSON 응답 반환 - 에러페이지 컨트롤러
...
//produces 속성: 클라이언트(header)가 보내는 accept 타입이 어떤것인지에 따라 해당 메서드를 호출.
@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");
//Jackson라이브러리가 Map을 JSON구조로 변환할 수 있기 때문에 Map으로 저장.
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);
//ResponseEntity를 사용하여 메시지 컨버터로 인해 클라이언트에 JSON이 반환된다. (스프링MVC - 메시지 컨버터 참고)
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
...
- produces = MediaType.APPLICATION_JSON_VALUE
- 클라이언트가 요청하는 HTTP Header의 Accept 값이 JSON(application/json)일 때 해당 메서드가 호출된다.
- Jackson 라이브러리는 Map을 JSON 구조로 변환할 수 있기 때문에 예외에 대한 정보를 Map에 담아 전송한다.
- ResponseEntity를 사용해서 메시지 컨버터로 인해 클라이언트에 JSON이 반환되게 된다.
API 예외 처리 구현 후, 서버 응답

- 다음과 같이 JSON 형태로 잘 반환이 된 것을 볼 수 있다.
스프링 부트 API 예외 처리
스프링 부트는 API 예외 처리도 스프링부트가 제공하는 기본 오류 방식(BasicErrorController)을 사용하여 자동화된 예외처리를 한다.
BasicErrorController 코드

- 스프링부트는 '/error'의 동일한 경로를 errorHtml(), error() 두 메서드로 처리하는 것을 알 수 있다.
- errorHtml() : produce : MediaType.TEXT_HTML_VALUE - 클라이언트의 예외처리 요청이 text/html인 경우, 해당 메서드가 호출된다. 반환타입을 보면 알 수 있듯이 뷰템플릿(ModelAndView)을 반환하게 된다.
- error() : 위의 경우를 제외하고 선 예외처리를 error()메서드를 사용한다. 스프링 부트는 예외 정보 반환을 ResponseEntity를 활용해 HTTP body에 JSON 형식으로 반환한다.
이처럼 스프링 부트는 BasicErrorController가 제공하는 기본 정보들을 활용해서 오류 API를 생성해준다.
스프링 부트의 BasicErrorController와 @ExceptionHandler를 활용한 API 오류 처리
- 스프링 부트에서 제공하는 BasicErrorController는 HTML 페이지에서 발생하는 오류 처리에 편리하게 사용된다. 이는 모든 4xx, 5xx 오류에 대해 잘 처리해주기 때문이다. 하지만 API에서 발생하는 오류 처리는 다른 차원의 고려사항이 필요하다.
- API는 각각의 컨트롤러나 예외마다 서로 다른 응답 결과를 출력해야 할 수 있다.
- 예를 들어, 회원과 관련된 API에서 발생하는 예외와 상품과 관련된 API에서 발생하는 예외의 응답 결과는 서로 달라질 수 있다.
- 이는 결과적으로 세밀하고 복잡한 처리가 필요하며, 이럴 때는 BasicErrorController보다는 @ExceptionHandler를 사용하는 것이 더 나은 방법이다.
'Spring > SpringMVC' 카테고리의 다른 글
| ExceptionResolver (0) | 2024.03.18 |
|---|---|
| HandlerExceptionResolver (0) | 2024.03.15 |
| 스프링 부트 - 오류페이지 처리 (0) | 2024.02.19 |
| 서블릿 예외처리 (0) | 2024.02.15 |
| 필터, 인터셉터 (0) | 2024.02.11 |