<Spring> 공통 에러 핸들러 구현하기
📌 스프링 에러 처리 방법 스프링은 프로그램 작동 중 여러 에러를 발생시킨다. 이에 에러를 처리할 수 있는 방법이 존재한다. 🔹 try-catch try { Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseCl
wtg1026.tistory.com
이 글에서 이어지는 내용입니다!
보고 오셔요!
📌 ResponseEntityExceptionHandler
🔵 ResponseEntityExceptionHandler 란?
A class with an @ExceptionHandler method that handles all Spring MVC
raised exceptions by returning a ResponseEntity with RFC 7807
formatted error details in the body.
Convenient as a base class of an @ControllerAdvice
for global exception handling in an application. Subclasses can override
individual methods that handle a specific exception, override
handleExceptionInternal to override common handling of all exceptions,
or override createResponseEntity to intercept the final step of creating
the ResponseEntity from the selected HTTP status code, headers, and body.
위는 스프링 패키지에서 살펴볼 수 있는, ResponseEntityExceptionHandler의 정의다.
이것은 스프링 프레임워크가 제공하는 예외 처리 클래스 중 하나이다.
위 정의에서 알 수 있듯이, Spring MVC에서 발생하는 에러를 처리해주는 클래스이다.
이를 사용하면 이전 포스트의 공통 에러 처리에서 수많은 에러를 하나하나 처리할 필요가 없어진다.
이 클래스가 대부분 처리해주기 때문이다.
🔵 어떻게 사용해야 할까?
ResponseEntityExceptionHandler가 어떻게 작동하는지 살펴보자
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
/** 생략 **/
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
if (ex instanceof HttpRequestMethodNotSupportedException subEx) {
return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
/** 생략 **/
}
@ExceptionHandler에 선언된 error가 발생할 경우 instance 종류를 구분하여 각각의 handler를 동작시킨다.
@Nullable
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
pageNotFoundLogger.warn(ex.getMessage());
return handleExceptionInternal(ex, null, headers, status, request);
}
이때 이 handler는 handleExceptionInternal이라는 함수를 실행한다.
@Nullable
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (request instanceof ServletWebRequest servletWebRequest) {
HttpServletResponse response = servletWebRequest.getResponse();
if (response != null && response.isCommitted()) {
if (logger.isWarnEnabled()) {
logger.warn("Response already committed. Ignoring: " + ex);
}
return null;
}
}
if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale());
}
return createResponseEntity(body, headers, statusCode, request);
}
매개변수로 예외 객체, 응답 본문, 응답 헤더, HTTP 상태 코드, 요청 객체 등을 받는다.
이 매개변수들을 에러 형식에 맞춰서 변환한 후,
ResponseEntity를 만들기 위해 createResponseEntity 메서드를 호출한다.
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
return new ResponseEntity<>(body, headers, statusCode);
}
받은 매개변수들을 이용하여, ResponseEntity 객체를 생성하여 반환합니다.
위 로직에서 error response를 만드는 부분은 handleExceptionInternal이라 할 수 있다.
handleExceptionInternal의 매개변수를 보면, body가 @nullable 처리되어있고,
이 함수의 로직을 살펴보면 body가 null이 아니면, 해당 body를 가지고 ResponseEntity를 만든다.
그러면 이 함수를 오버라이딩해서 body를 커스텀해서 넘긴다면,
error response를 커스텀할 수 있지 않을까?
구현해보도록 하자.
📌 구현하기
🔵 GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex,
@Nullable Object body,
HttpHeaders headers,
HttpStatusCode statusCode,
WebRequest request) {
log.error("HandleInternalException", ex);
final HttpStatus status = (HttpStatus) statusCode;
final ErrorReason errorReason = ErrorReason.from(status.value(), status.name(), ex.getMessage());
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return super.handleExceptionInternal(ex, errorResponse, headers, status, request);
}
// 비즈니스 로직 에러 처리
@ExceptionHandler(BaseErrorException.class)
public ResponseEntity<ErrorResponse> handleBaseErrorException(BaseErrorException e, HttpServletRequest request) {
log.error("BaseErrorException", e);
final ErrorReason errorReason = e.getErrorCode().getErrorReason();
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
// 위에서 따로 처리하지 않은 에러를 모두 처리해줍니다.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) {
log.error("Exception", e);
final GlobalErrorCode globalErrorCode = GlobalErrorCode._INTERNAL_SERVER_ERROR;
final ErrorReason errorReason = globalErrorCode.getErrorReason();
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
아래 두 @ExceptionHandler는 비즈니스 로직 에러와 따로 처리하지 않은 에러 즉, 500번 에러를 처리해주는 부분이다.
이 공통에러 처리에서 중요한 부분은 handleExceptionInternal를 오버라이드한 부분이다.
HttpStatus와 Exception에서 ErrorResponse를 만들기 위한 정보를 얻은 후,
ErrorResponse를 만들어서 기존 handleExceptionInternal의 body로 넘겨준다.
이렇게 할 경우, ErrorResponse가 response의 body에 넣어져서
클라이언트에 전달될 수 있다.
이런 방식으로 공통 에러 처리를 구현할 수 있다.
아래는 ErrorReason과 ErrorResponse 코드입니다.
참고하세요!
🔵ErrorReason
@Getter
public class ErrorReason {
private final Integer status;
private final String code;
private final String reason;
@Builder
private ErrorReason(Integer status, String code, String reason) {
this.status = status;
this.code = code;
this.reason = reason;
}
public static ErrorReason from(Integer status, String code, String reason) {
return ErrorReason.builder()
.status(status)
.code(code)
.reason(reason)
.build();
}
}
🔵ErrorResponse
@Getter
public class ErrorResponse {
private final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
private final boolean success = false;
private String code;
private String status;
private String reason;
@Builder
private ErrorResponse(String code, String status, String reason) {
this.code = code;
this.status = status;
this.reason = reason;
}
public static ErrorResponse of(ErrorReason errorReason) {
return ErrorResponse.builder()
.code(errorReason.getCode())
.status(errorReason.getStatus().toString())
.reason(errorReason.getReason())
.build();
}
}
'Backend' 카테고리의 다른 글
<Spring> Redis 기반 검색어 자동 완성 기능 구현하기 (1) | 2023.10.01 |
---|---|
<Spring> AOP 기반 분산락 (0) | 2023.10.01 |
<Spring> HttpServletRequest 값 사라지는 문제 (1) | 2023.05.06 |
<Spring> Success Custom Response 적용하기 (0) | 2023.05.02 |
<Spring> 공통 에러 핸들러 구현하기 (1) | 2023.03.28 |
<Spring> 공통 에러 핸들러 구현하기
📌 스프링 에러 처리 방법 스프링은 프로그램 작동 중 여러 에러를 발생시킨다. 이에 에러를 처리할 수 있는 방법이 존재한다. 🔹 try-catch try { Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseCl
wtg1026.tistory.com
이 글에서 이어지는 내용입니다!
보고 오셔요!
📌 ResponseEntityExceptionHandler
🔵 ResponseEntityExceptionHandler 란?
A class with an @ExceptionHandler method that handles all Spring MVC
raised exceptions by returning a ResponseEntity with RFC 7807
formatted error details in the body.
Convenient as a base class of an @ControllerAdvice
for global exception handling in an application. Subclasses can override
individual methods that handle a specific exception, override
handleExceptionInternal to override common handling of all exceptions,
or override createResponseEntity to intercept the final step of creating
the ResponseEntity from the selected HTTP status code, headers, and body.
위는 스프링 패키지에서 살펴볼 수 있는, ResponseEntityExceptionHandler의 정의다.
이것은 스프링 프레임워크가 제공하는 예외 처리 클래스 중 하나이다.
위 정의에서 알 수 있듯이, Spring MVC에서 발생하는 에러를 처리해주는 클래스이다.
이를 사용하면 이전 포스트의 공통 에러 처리에서 수많은 에러를 하나하나 처리할 필요가 없어진다.
이 클래스가 대부분 처리해주기 때문이다.
🔵 어떻게 사용해야 할까?
ResponseEntityExceptionHandler가 어떻게 작동하는지 살펴보자
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
/** 생략 **/
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
if (ex instanceof HttpRequestMethodNotSupportedException subEx) {
return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
/** 생략 **/
}
@ExceptionHandler에 선언된 error가 발생할 경우 instance 종류를 구분하여 각각의 handler를 동작시킨다.
@Nullable
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
pageNotFoundLogger.warn(ex.getMessage());
return handleExceptionInternal(ex, null, headers, status, request);
}
이때 이 handler는 handleExceptionInternal이라는 함수를 실행한다.
@Nullable
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (request instanceof ServletWebRequest servletWebRequest) {
HttpServletResponse response = servletWebRequest.getResponse();
if (response != null && response.isCommitted()) {
if (logger.isWarnEnabled()) {
logger.warn("Response already committed. Ignoring: " + ex);
}
return null;
}
}
if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale());
}
return createResponseEntity(body, headers, statusCode, request);
}
매개변수로 예외 객체, 응답 본문, 응답 헤더, HTTP 상태 코드, 요청 객체 등을 받는다.
이 매개변수들을 에러 형식에 맞춰서 변환한 후,
ResponseEntity를 만들기 위해 createResponseEntity 메서드를 호출한다.
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
return new ResponseEntity<>(body, headers, statusCode);
}
받은 매개변수들을 이용하여, ResponseEntity 객체를 생성하여 반환합니다.
위 로직에서 error response를 만드는 부분은 handleExceptionInternal이라 할 수 있다.
handleExceptionInternal의 매개변수를 보면, body가 @nullable 처리되어있고,
이 함수의 로직을 살펴보면 body가 null이 아니면, 해당 body를 가지고 ResponseEntity를 만든다.
그러면 이 함수를 오버라이딩해서 body를 커스텀해서 넘긴다면,
error response를 커스텀할 수 있지 않을까?
구현해보도록 하자.
📌 구현하기
🔵 GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex,
@Nullable Object body,
HttpHeaders headers,
HttpStatusCode statusCode,
WebRequest request) {
log.error("HandleInternalException", ex);
final HttpStatus status = (HttpStatus) statusCode;
final ErrorReason errorReason = ErrorReason.from(status.value(), status.name(), ex.getMessage());
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return super.handleExceptionInternal(ex, errorResponse, headers, status, request);
}
// 비즈니스 로직 에러 처리
@ExceptionHandler(BaseErrorException.class)
public ResponseEntity<ErrorResponse> handleBaseErrorException(BaseErrorException e, HttpServletRequest request) {
log.error("BaseErrorException", e);
final ErrorReason errorReason = e.getErrorCode().getErrorReason();
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
// 위에서 따로 처리하지 않은 에러를 모두 처리해줍니다.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) {
log.error("Exception", e);
final GlobalErrorCode globalErrorCode = GlobalErrorCode._INTERNAL_SERVER_ERROR;
final ErrorReason errorReason = globalErrorCode.getErrorReason();
final ErrorResponse errorResponse = ErrorResponse.of(errorReason);
return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
아래 두 @ExceptionHandler는 비즈니스 로직 에러와 따로 처리하지 않은 에러 즉, 500번 에러를 처리해주는 부분이다.
이 공통에러 처리에서 중요한 부분은 handleExceptionInternal를 오버라이드한 부분이다.
HttpStatus와 Exception에서 ErrorResponse를 만들기 위한 정보를 얻은 후,
ErrorResponse를 만들어서 기존 handleExceptionInternal의 body로 넘겨준다.
이렇게 할 경우, ErrorResponse가 response의 body에 넣어져서
클라이언트에 전달될 수 있다.
이런 방식으로 공통 에러 처리를 구현할 수 있다.
아래는 ErrorReason과 ErrorResponse 코드입니다.
참고하세요!
🔵ErrorReason
@Getter
public class ErrorReason {
private final Integer status;
private final String code;
private final String reason;
@Builder
private ErrorReason(Integer status, String code, String reason) {
this.status = status;
this.code = code;
this.reason = reason;
}
public static ErrorReason from(Integer status, String code, String reason) {
return ErrorReason.builder()
.status(status)
.code(code)
.reason(reason)
.build();
}
}
🔵ErrorResponse
@Getter
public class ErrorResponse {
private final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
private final boolean success = false;
private String code;
private String status;
private String reason;
@Builder
private ErrorResponse(String code, String status, String reason) {
this.code = code;
this.status = status;
this.reason = reason;
}
public static ErrorResponse of(ErrorReason errorReason) {
return ErrorResponse.builder()
.code(errorReason.getCode())
.status(errorReason.getStatus().toString())
.reason(errorReason.getReason())
.build();
}
}
'Backend' 카테고리의 다른 글
<Spring> Redis 기반 검색어 자동 완성 기능 구현하기 (1) | 2023.10.01 |
---|---|
<Spring> AOP 기반 분산락 (0) | 2023.10.01 |
<Spring> HttpServletRequest 값 사라지는 문제 (1) | 2023.05.06 |
<Spring> Success Custom Response 적용하기 (0) | 2023.05.02 |
<Spring> 공통 에러 핸들러 구현하기 (1) | 2023.03.28 |