이 포스팅을 읽기전에 이전의 작성한 포스팅들과 연관되어 설명하는 것이 많기때문에 이전 포스팅들을 읽고오는 것을 추천한다.
아래 링크는 포스팅 순서대로 첨부한 것이다.
2024.09.04 - [Framework/Spring] - [Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답
2024.09.06 - [Framework/Spring] - [Spring] - API예외 처리를 이해하기위한 기본 개념!
2024.09.07 - [Framework/Spring] - [Spring] - 스프링에서 예외처리를 위한 'HandlerExceptionResolver' 알아보기
자 이제 기본개념은 알겠으니 좀 더 잘 활용하도록 심화과정을 학습해보자!
사실 지금까지는 공부한 바로는 예외가 발생하면 WAS까지 갔다가 WAS에서 다시 오류 매핑을 위해 예외 정보를 가지고 다시 내려간다
이 과정은 매우 복잡하고 비효율적이다. ExceptionResolver
를 깊게 활용하여 이런 복잡한 과정없이 깔끔하게 예외를 관리해보자.
코드 직접 구현해보기
먼저 UserException
이라는 커스텀 예외를 하나 구현하자.
public class UserException extends RuntimeException{
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
컨트롤러에는 user-ex
가 들어왔을때 우리가 커스텀한 예외가 발생하도록 설정해주자.
이제 다시 HandlerExceptionResolver
를 구현하는 클래스를 작성하면된다.
코드를 작성하기전에 먼저 한번 생각해보자,
- 구현하는
ExceptionResolver
는UsreException
예외를 처리하기위해 구현한다. - 500 Internal Server Error를 400 Bad Request 상태코드로 전환한다.
- 이제는 API예외응답도 구현해야하기때문에 헤더정보를 확인해 API응답과 HTML응답을 구분한다.
- API예외 응답을 하기위해서는 JSON형태로 응답을 변경해주어야한다.
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter()
.write(result);
return new ModelAndView();
} else {
return new ModelAndView("/error-page/500");
}
}
} catch (IOException e) {
log.error("ersolver ex", e);
}
return null;
}
}
실제 구현한 코드로 익숙한 부분들이 많이 보일 것이다.
먼저 API예외 응답은 JSON으로 출력해야하기때문에 ObjectMapper
를 이용한다.
응답객체에 임의로 지정하는 상태코드 SC_BAC_REQUEST를 설정해준다.
그리고 UserException
이 발생하면 해당 요청의 HTTP 헤더의 Accept
값을 읽고 해당 요청이 HTML페이지 응답을 원하는지, JSON형식의 응답을 원하는지 파악한다.
JSON을 반환하기위한 형태인 Map
컬렉션을 만들어주고 해당 컬렉션에 반환하고자하는 데이터를 담아준다.
그리고나서 API응답이기때문에 빈 ModelAndView
를 반환해주기만하면 끝이다 !
HTML페이지를 응답하기위해서는 단순히 템플릿 경로를 ModelAndView
의 인자로 넣어주기만하면 된다.
지금까지 UserException
이라는 단 하나의 예외를 처리하기위해
HTTP헤더 조회..
헤더 Accept비교..
반환하기위한 JSON 생성..
응답객체 조작..
등 복잡한 작업들을 직접 구현해주었다.
프로젝트에서 처리하고자하는 예외가 수십가지가 된다면 이 코드를 일일히 작성해주어야한다.
스프링은 이런 불편함을 해결해주기위해 역시 어노테이션 기반의 예외처리방법을 제공해준다!
스프링이 제공하는 어노테이션으로 예외 잡기 - @ResponseStatus
우리는 BadRequestException
이라는 커스텀 예외를 @ResponseStatus
로 처리해볼 것이다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException{
public BadRequestException() {
super();
}
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
public BadRequestException(Throwable cause) {
super(cause);
}
protected BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
BadRequestException
이라는 RuntimeException
을 상속하는 커스텀 예외를 생성해주고@ResponseStatus
어노테이션을 클래스레벨에 붙혀주고 원하는 상태코드와 예외 메시지를 입력해주면 끝이다.
너무 간단하다..
이전에 우리는 커스텀 예외를 만들고, HandlerExceptionResolver
를 구현하는 resolver
를 만들어주고.. json매핑, 응답객체 조작, 상태코드 입력등을 직접해주었지만@ResponseStatus
어노테이션만 사용하면 이전에 직접구현했던 동작들을 스프링에서 알아서 동작하도록 구현해놓았다.
결론적으로 @ResponseStatus
에 담긴 code속성과 reason속성의 값들을sendError(code, reason)
의 형태로 설정해주고 똑같이 빈 ModelAndView
를 반환해주는 것이 @ResponseStatus
의 동작 방법이다 !!
reason속성을 messages.properties로!!
reason에 들어가는 값을 /resources의 messages.properites속성 파일에 미리 정의해서 변수형태로도 사용할 수 있다.
이렇게 관리하면 조금 더 편리하게 예외 메시지를 유지보수 할 수 있을 것이다.
라이브러리 예외, 시스템 예외와 같이 우리가 정의할 수 없는 예외처리는?NullPointException
이라던가 IllegalArgumentException
과 같이 라이브러리에서 제공하거나 시스템 자체에서 제공해주는 예외에는 @ResponseStatus
와 같은 어노테이션을 붙힐수 없다.
이미 정의되어있는 예외를 처리하기위해서는 ResponseStatusException
이라는 스프링이 제공하는 예외를 사용해야한다.ResponseStatusException
은 파라미터로
- 응답하고자하는 상태코드
- 예외 메시지
- 처리하고자하는 라이브러리 또는 시스템 예외 종류
로 이루어져있다.
만약NullPointException
을 우리가 임의로 핸들링하려면ResponseStatusException
으로NullPointerException
을 감싸는 것이다.
아래의 예제 코드를 보면 바로 이해가 될 것이다.
IllegalArgumentException
을 처리하기위해 ResponseStatusException
으로 한번 감싸고 메시지, 상태코드를 입력해주었다.
이렇게 되면 IllegalArgumentException
예외가 발생하면 해당 예외가 터지지않고 ResponseStatusException
예외로 대체하고 스프링은 그 예외를 똑같이 sendError(404, "객체 값이 NULL입니다.")
의 형태로 처리해버린다.
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "객체 값이 NULL입니다.", new IllegalArgumentException());
}
실제 결과를 확인해보면 서버로그에 IllegalArgumentException
이나 ResponseStatusException
예외가 터지지 않는 것을 확인할수 있으며, 예외 응답도 API로 잘 처리되는 것을 확인할 수 있다.
ResponseStatusException
이나 @ResponseStatus
는 상태코드를 임의로 지정하고 예외 흐름을 정상흐름으로 바꾸어 WAS에게 전달할 뿐 API응답을 위한 JSON으로 반환하는 어노테이션인 @ExceptionHanlder
가 있다. 이 어노테이션은 매우 자주 사용되고 중요하기때문에 후에 따로 설명한다
DefaultHandlerExceptionResolver
스프링이 제공해주는 ResponseStatusException
을 통하여 라이브러리나 프레임워크, 시스템이 제공하는 예외도 처리할 수 있다고 하였다.
실제로 자바의 NPE와 같은 널리알려진 예외를 직접 처리해보는 예제를 살펴보았었다.
하지만 스프링프레임워크 내부적으로 발생하는 예외는 어떨까?
스프링 프레임워크에 어떤 예외가 있는지 알고 있는 사람들은 있을수 있지만, NPE와 같이 널리 알려지 있지는 않을 것이다.
때문에 해결하기위해서는 방법을 연구해야하고 해당 예외가 어떻게 발생하는지도 알아야하는데 스프링에는 예외만 수십수백가지 될 것이고 이것에 대한 예외를 처리하기에는 너무 복잡하고 어렵다.
아래 코드를 살펴보자
@GetMapping("/api/response-status-spring")
public String responseStatusSpring() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "객체 값이 NULL입니다.", new TypeMismatchException());
}
이런식으로 ResponseStatusException
으로 예외를 잡을 수 있지만, new TypeMismatchException
구문에서 컴파일에러가 발생하는데 TypeMismatchException
의 기본 생성자가 존재하지않고 매개변수가 몇 개 존재하기때문이다.
즉 TypeMisMatchException
을 직접 잡기위해서는 TypeMisMatchException
에 대한 지식이 있어야한다는 것이다.
때문에 스프링은 DefaultHandlerExceptionResolver
를 제공하여 스프링 프레임워크 내부에서 발생하는 예외들을 직접 처리하고, 404, 500상태코드로 변환하여 예외를 반환해 개발자들이 좀 더 명확한 예외를 확인할 수 있도록 한다.
가장 쉬운 예외인 TypeMismatchException
을 예제로 살펴보자TypeMismatchException
을 파라미터에 맞지 않는 요청이 들어왔을 때 발생하는 스프링 예외이다.
@ResponseBody
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
예를 들어 위와같은 컨트롤러 메서드가있을때 data
변수는 반드시 클라이언트(사용자)로부터 받아오는 값이다.data
파라미터는 Integer
인 숫자타입을 받아야하지만 클라이언트에서 String
과 같은 문자열을 입력할 경우에는 TypeMismatchException
이 발생하게 되는 것이다.
여기서 잠깐 생각을 해보자.
지금 예외가 발생한 원인은 사용자가 data
매개변수에 들어올 값의 타입을 잘못입력한 것이다.
하지만 해당예외는 결과적으로 컨트롤러에서 발생했기때문에 WAS는 500 Internal Server Error로 인지하게 된다.
아니 사용자가 입력을 잘못했으니까 400Bad Request를 반환해야하는데 왜 500 Internal Server Error가 나가지..
이런 불만을 스프링이 DefaultHandlerExceptionResolver
로 해결해놓았다.
실제로 위 코드를 실행해보자
실행결과
{
"timestamp": "2024-09-06T04:18:17.687+00:00",
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
"message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; For input string: \"qqq\"",
"path": "/api/default-handler-ex"
}
상태코드로 400 BadRequest가 반환되는 것을 확인할 수 있다.
또 서버로그를 확인해보자
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; For input string: "qqq"]
자세히보면 DefaultHandlerExceptionResolver
가 호출된 것을 확인할 수 있다.
이런식으로 스프링은 스프링 프레임워크 내부에서 발생하는 복잡한 여러 예외들을 해당 예외 발생 상황에 맞게 옳바르게 정의해놓았다.
TypeMismatchException
과 같이 클라이언트로인해 발생한 예외를 500예외가 아닌 400으로 치환하여 상태코드를 반환하는게 바로 대표적인 예시이다.
'Framework > Spring' 카테고리의 다른 글
[Spring] - 스프링에서 예외처리를 위한 'HandlerExceptionResolver' 알아보기 (1) | 2024.09.07 |
---|---|
[Spring] - API예외 처리를 이해하기위한 기본 개념! (2) | 2024.09.06 |
[Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답 (0) | 2024.09.04 |
[Spring] 서블릿 필터와 스프링 인터셉터 비교하기! (0) | 2024.08.07 |
[Spring] JdbcTemplate (0) | 2024.06.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!