이 글은 Spring에서의 예외 처리에 @ExceptionHandler을 사용하는 이유부터 사용방법, 실제 프로젝트 적용 방법을 정리한 내용입니다. 김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 참고하였습니다. 📝
스프링에서의 예외 처리 - @ExceptionHandler?
웹사이트를 하다보면 종종 이런 에러페이지를 마주친 적이 있으셨을 겁니다.
Spring에서는 기본적으로는 오류 발생시, 위처럼 BasicErrorController
에 등록된 기본 에러 페이지(/error)를 리턴해줍니다. 하지만 만약 프로그램에서 발생한 모든 에러를 에러 페이지로 매핑시켜 준다면 각 API들의 디테일한 에러 상황을 알려주지 못하게 됩니다.
BasicErrorController
를 활용해서 json형식으로 반환해줄 수도 있지만, API 오류 처리는 보다 더 디테일한 처리가 필요하기 때문에 BasicErrorController
이 아니라 @ExceptionHandler
을 사용합니다.
예를 들면, 아래는 회원가입하려는 사용자가 입력한 아이디가 이미 다른 회원이 사용하고 있는 아이디인 상황(중복 아이디)입니다.
만약 BasicErrorController
을 사용할 경우, WAS를 거쳐 서버에서 발생한 에러이기에 500 상태코드가 발생하게 됩니다. 하지만 구체적으로는 사용자의 요청(중복된 아이디)이 잘못되었기 때문에 400에러를 전달해주는 게 보다 정확해 보입니다. 그래서 조금 더 디테일하게 400 상태코드와 관련 에러 메시지를 브라우저에게 전달해주고자 합니다.
이렇게 각각의 API 에러마다 구체적인 에러 메시지와 정확한 상태코드를 전달해주기 위해 @ExceptionHandler
을 사용합니다.
ExceptionResolver? Spring의 에러 해결사!
@ExceptionHandler
에 대해 알아보기 전에, ExceptionResolver
에 대해서 간단히 정리해보겠습니다. ExceptionResolver
(= HandlerExceptionResolver)는 API마다 구체적으로 예외 처리를 할 수 있는 말 그대로 예외 해결사(ExceptionResolver)입니다.
우선 앞서 BasicErrorController
의 경우, 컨트롤러에서 예외 발생 시 WAS까지 쭉~ 해당 에외가 전달됩니다. 그러면 WAS에서 관련 에러 페이지를 매핑해서 반환해주는 과정을 거칩니다. 복잡한 과정일 뿐만 아니라, WAS에서 에러가 발생하였으므로 500 상태코드만 반환됩니다.
반면 ExceptionResolver
을 사용할 경우, 예외 발생 시 해당 예외가 WAS까지 전달되지 않습니다. 디스패처 서블릿에 예외가 전달되면, ExceptionResolver을 호출하여 (개발자가 짜놓은 대로) 예외 처리하게 됩니다. 그래서 WAS에는 예외가 전달되지 않고, 정상 응답을 하게 됩니다. 이떄, ExceptionResolver에서 500 상태 코드가 아니라 개발자가 지정한 상태 코드를 응답할 수 있습니다. (물론 관련 에러 메시지도 함께 응답으로 지정할 수 있습니다.)
Spring은 기본적으로 ExceptionResolver
가 몇 가지 등록되어 있습니다. 다음 우선 순위로 처리됩니다. 앞서 나온 @ExceptionHandler
도 ExceptionResolver
의 한 종류입니다.
ExceptionHandlerExceptionResolver
→ 이를 주로 사용합니다! ⚡ResponseStatusExceptionResolver
- HTTP 상태 코드를 지정해줄 수 있습니다.
- 예) @ResponseStatus(value = HttpStatus.BAD_REQUEST)
DefaultHandlerExceptionResolver
3번의 경우 ModelAndView로 반환하기에 각각의 디테일한 API 에러 처리에 적절하지 않고, 직접 response에 응답코드, 메시지 등등을 넣어주어야 해서 사용하기 번거롭다는 단점이 있습니다. 따라서 API 예외 처리에는 @ExceptionHandler
을 주로 사용한다고 합니다.
본론, ExceptionHandler 사용하기
앞서 나왔던 BasicErrorController
이나 HandlerExceptionResolver
의 경우 각각 번거로운 점이 있기 때문에, API 예외 처리할 때는 주로 @ExceptionHandler
를 사용합니다.
Error 관련된 정보를 담아두는 Dto
우선 에러 발생 시 응답으로 보낼 데이터들을 Dto로 만들었습니다. (예시와 방식은 김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고하였습니다.)
@ExceptionHandler을 사용한 예외 처리 방식 2가지
컨트롤러단에 아래 예시처럼 @ExceptionHandler
가 달린 메소드를 구현해주어야 합니다. 해당 컨트롤러에서 예외가 발생할 경우, 이 메소드들이 호출됩니다.
1. ErrorResult Dto 자체를 반환하고, 상태 코드는 @ResponseStatus
에 입력
이때 예외가 발생했지만, 이를 @ExceptionHandler
를 통해 (WAS에 에러 반환하지 않고) 정상 처리했으므로 상태 코드 200을 반환합니다. 따라서 프론트에 4XX등의 예외 상태 코드로 반환해주어야 하는 경우, @ResponseStatus
를 사용하여 상태 코드를 따로 설정해주어야 합니다.
위 경우(IllegalArgumentException
)는 사용자의 잘못된 입력으로 예외가 발생하였으므로, @ResponseStatus(HttpStatus.BAD_REQUEST) // 상태 코드 400
을 사용해주었습니다.
참고로 제가 주로 사용할 상태코드를 정리해보았습니다 📝
- 4XX : 사용자(브라우저)의 잘못된 요청
- 400 (BAD_REQUEST) : 사용자의 잘못된 입력
- 401 (UNAUTHORIZED) : 잘못된 권한 접근
- 403 (FORBIDDEN) : 접근 금지
- 404 (NOT_FOUND) : 해당 url이 없을 경우
-
5XX : 서버에서의 에러
- 500 (INTERNAL_SERVER_ERROR) : 서버 내부의 문제일 경우
- 501 (NOT_IMPLEMENTED) : 서버가 해당 요청(자원)을 지원하지 않을 경우
- 503 (SERVICE_UNAVAILABLE) : 서버가 요청 처리할 준비가 되지 않은 경우 (요청이 너무 많을 경우 등)
- 504 (GATEWAY_TIMEOUT) : 유효한 제한 시간 이내에 응답하지 않은 경우
- 참고 공식 문서 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html
2. ResponseEntity에 담아서 상태코드와 함께 반환
아래 코드처럼 ResponseEntity에 담아서 상태코드와 함께 반환해줄 수도 있습니다.
이러한 두가지 방식으로 @ExceptionHandler을 사용한 메소드들을 컨트롤러단에 넣어주면, 해당 컨트롤러 안에서 예외가 발생할 경우, 이 로직들이 호출됩니다.
@RestControllerAdvice 사용하기
이렇게 매번 컨트롤러마다 @ExceptionHandler
을 구현하는 게 번거로우므로, 보통 여러 컨트롤러에 함께 적용할 수 있는 @RestControllerAdvice
을 사용합니다.
@RestControllerAdvice
에 옵션을 달아주지 않을 경우, 모든 컨트롤러에 적용됩니다.
보통 옵션에는 아래처럼 컨트롤러 클래스를 걸어주거나, 패키지를 넣어주거나, 해당 @ControllerAdvice를 사용할 클래스들을 걸어줍니다.
@ControllerAdvice(annotations = RestController.class)
@ControllerAdvice("com.rememberme")
@ControllerAdvice(assignableTypes = {A.class, B.class})
실제 프로젝트에 적용하기 - 에러 메시지/상태코드 설정
강의와 구글링으로 배웠던 내용들을 바탕으로 진행중인 Remember-me 프로젝트 예외 처리에 적용해주었습니다. @ExceptionHandler
와 @RestControllerAdvice
을 사용하여 각각의 API마다 예외 처리를 해주었습니다. 그래서 WAS까지 내려가지 않고 예외 처리하며, 제가 설정한 Dto를 반환해주고 원하는 상태코드도 함께 설정해주었습니다.
아래처럼 에러 메시지는 로직별로 설정해주었고, 상태코드는 예외 타입별로 지정해주었습니다.
UserService.java
일부 → 로직별로 에러메시지 설정
ExceptionControllerAdvice.java
일부 → 각 예외별 상태 코드 지정
@Valid + @Pattern 사용 시, 내가 원하는 에러 message만 반환하기
프로젝트에서 사용자의 입력 형식을 검증하기 위해 주로 @Valid
과 @Pattern
을 사용해주었습니다.
@Vaild
의 경우, 예외 상황에서 MethodArgumentNotValidException
을 반환해줍니다. 그리고 기본적으로는 아래 이미지처럼 긴.. 에러메시지를 모두 반환해줍니다. 전 여기서 간단하게 제가 @Pattern
에 지정한 에러 메시지만을 반환하고 싶었습니다.
Baeldung의 예제 코드를 참고하여 MethodArgumentNotValidException
의 getBindingResult()
으로 BindingResult
객체를 꺼내고(이 객체는 Valid 검증 오류가 발생 시, 해당 오류 내용을 담고 있습니다), 다시 getFieldErrors()
으로 에러 내용을 꺼낸 뒤, getDefaultMessage()
으로 제가 지정한 에러 메시지만을 꺼냈습니다.
- 참고 자료(Baeldung) : https://www.baeldung.com/global-error-handler-in-a-spring-rest-api
여기까지 Spring에서 API별 예외 처리에 왜 @ExceptionHandler
을 사용하는 지 정리해보았고, 이를 프로젝트에 적용하는 과정까지 정리해보았습니다 :)