이 포스팅은 이전에 작성한 아래 포스팅을 읽었다는 가정하에 작성한다.
이전포스팅을 읽어야만 현재 포스팅이 이해되는 것은 아니지만 글의 흐름을 이전포스팅과 연관지어서 작성하며 코드를 이전 포스팅의 것을 재사용하기때문에 이전 포스팅을 읽어보는것을 권장한다 !
[Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답
스프링을 사용하지 않는 순수 서블릿 컨테이너는 Exception과 response.sendError(Http상태코드, 오류메시지)두 가지 방식으로 예외를 처리한다. Exception으로 처리하기기본적으로 자바는 예외가 발생하
lee-dev-log.tistory.com
이전 포스팅에서는 예외 처리, HTTP 상태코드에 맞는 에러 페이지를 작성하여 /templates
디렉터리에 보관하였고, 해당 페이지가 WAS, 서블릿 필터, 인터셉터를 거치며 어떤 동작을 수행하는지 어떻게 예외 페이지를 보여주는지 알아보았고 추가로 스프링 부트를 통해 이러한 설정을 '딸깍'으로 어떻게 설정했는지 알아보았다.
하지만 요즘 웹 애플리케이션은 단순히 뷰 템플릿을 통한 서버사이드 렌더링을 하기보다는 React를 활용한 SPA, 또는 MSA를 위한 서버간의 데이터 통신, 모바일 앱과의 데이터 통신을 위해 API 통신과 같은 웹 애플리케이션이 대다수라고 할 수 있다. (JSON 또는 XML 양식을 통한 데이터 통신)
그렇기때문에 우리는 예외에 맞는 에러 페이지를 어떻게 보여주어야하는가? 뿐만아니라 API예외 처리는 어떻게하고 어떤 모양으로 통신하는 서버에게 예외가 났음을 알리며 그에 대한 예외를 어떻게 보여줄 것인가? 를 더 확실하게 이해해야한다.
본격적으로 학습에 들어가기에 앞서 이전에 작성했던 코드들의 주석을 풀어주자
예외발생시 보여줄 에러페이지를 등록한 WebServerCustomizer
의 @Component
어노테이션을 사용해주자
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageRuntime = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageRuntime);
}
}
주석을 해제했다면 이제 뷰 템플릿을 응답하기위한 컨트롤러가아닌 JSON을 응답하기위한 컨트롤러를 만들어보자
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello " + id);
}
}
@RestController
는 @Controller
+ @ResponseBody
가 합쳐진 편의 어노테이션이다.
이 어노테이션을 사용하면 컨트롤러에서 응답이 메시지 바디를 통해 응답한다.
getMember()
메서드는 /api/members/{id}
요청이 들어오면 {id}의 값이 "ex"일 경우 임의로 런타임예외를 발생시키고 그렇지 않으면 정상흐름으로 DTO객체를 응답으로 반환한다.
이 때 MemberDto객체는 JSON으로 반환되는데 이는 스프링이 내부적으로 ObjectMapper
를 통해 객체를 JSON으로 변환하는 로직을 자동으로 수행해준다.
우리가 이전 포스팅에서 RuntimeException
이 발생하면 어떤 로직이 수행되도록 처리했는지 기억하는가?
ErrorPage errorPageRuntime = new ErrorPage(RuntimeException.class, "/error-page/500");
이 에러 페이지를 등록해두었고 예외가 발생하면 WAS에서 다시 요청을 보내 해당 에러 페이지를 응답하도록 하였다.
그렇기때문에 현재에 만약 "ex"값이 들어올 경우 따로 JSON형식으로 응답을 클라이언트로 보내는 것이아니라 HTML파일이 클라이언트로 응답가는 것을 확인할 수 있을 것이다.
HTML파일로 응답을 보내면 받는 사람입장에서는 HTML코드를 보기좋게 보이게 한것도아니고..
그냥 HTML코드 그 자체만 보인다.
이러면 예외에 대한 결과를 받는 입장에서는 난해하고 어떻게 해야할지를 모를 것이다..
그렇기때문에 예외가 발생했을 때 통신하는 서버간에 정해진 형식의 JSON(또는 XML)으로 응답을 보내주어야한다.
RequestMapping의 produces
속성
만약 GET - /error-page/500
리소스에 대해서 HTML페이지로 보여주어야하는 예외처리가 있고 JSON형식으로도 보내야하는 예외처리가 있다면,
GET - /error-page/500-html
GET - /error-page/500-json
위와 같이 두 개의 컨트롤러 매핑 메서드를 작성해주어야한다.
이렇게되면 리소스 경로에 대한 고민을하고 네이밍도 고민해야하기때문에 번거로 울 수 있다.
이 때produces
속성을 사용하면 요청 미디어타입에 맞는 예외처리를 할 수 있다.
@RequestMapping(value = "/error-page/500", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletRequest response) {
log.info("API errorPage 500");
Map<String, Object> result = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
result.put("message", ex.getMessage());
result.put("status", request.getAttribute(ERROR_STATUS_CODE));
Integer statusCode = (Integer)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
@RequestMapping("/error-page/500")
public String errorPageHtml() {
//로직
return "/error-page/500";
}
errorPage500Api
메서드와 errorPageHtml
메서드의 매핑하는 URI경로는 같지만,errorPage500Api
메서드는 produces
속성의 값으로 JSON을 받는 것을 확인할 수 있다.
스프링부트는 설정에 대해 광범위한 설정보다 디테일한 설정을 우선으로 설정하기때문에 JSON요청이 들어오면 errorPage500Api
메서드를, 그 외의 요청에 대한 예외를 errorPageHtml
로 처리한다.
참고로 이렇게 API에 대한 예외와 HTML에 대한 예외를 같이 사용한다면 클래스레벨에 @RestController
를 사용해서는 안되고 클래스레벨에는 @Controller
를 사용하고 JSON예외 처리 메서드는 메서드레벨에 @ResponseBody
어노테이션을 붙혀주어야한다.
이제 기본적인 동작원리를 이해했다면 스프링부트는 어떻게 오류처리를 제공하는지 알아보자!
먼저 WebServerCustomizer
의 @Component
를 비활성화 해주자
그리고나서 그대로 실행을 해보고 파라미터를 ex로 주어 의도적으로 예외를 발생시켜보자
{
"timestamp": "2024-09-05T11:30:46.893+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/members/ex"
}
그러면 위와 같이 좀 더 다양한 예외 정보들이 출력되고 있다.
이 또한 스프링부트가 개발하기 편하도록 기본적인 예외처리에 대해 미리 구현해놓았기때문에 그것을 사용하기만 하면되는것이다.
그 구현은 역시 BasicErrorController
에 구현되어있다.
BasicErrorController 살짝 살펴보기
아래는 실제 스프링에서 구현한 BasicErrorController
코드의 일부이다
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
메서드 이름만봐도 알 수 있듯이 errorHtml
메서드는 HTML예외 반환
즉, HTTP 헤더의 Accept값이 html일 경우에 동작하는 메서드이다.
반환 타입을 ModelAndView
로 선언해두었기에 View가 반환되어 클라이언트에 HTML페이지를 반환하는 것이다. 우리가 직접 구현해본것과는 다르게 produces의 속성을 "text/html"로 주었다.
다음으로 error
메서드의 반환타입은 ResponseEntity
타입을 반환하여 HTTP 메시지 바디에 직접 데이터를 담아서 클라이언트에 반환하게 된다.
'Framework > Spring' 카테고리의 다른 글
[Spring] - HandlerExceptionResolver 활용하여 예외처리하기 (2) | 2024.09.07 |
---|---|
[Spring] - 스프링에서 예외처리를 위한 'HandlerExceptionResolver' 알아보기 (1) | 2024.09.07 |
[Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답 (0) | 2024.09.04 |
[Spring] 서블릿 필터와 스프링 인터셉터 비교하기! (0) | 2024.08.07 |
[Spring] JdbcTemplate (0) | 2024.06.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!