🤔 서론
Exception Handler 를 구현하다 리펙토링 한 경험을 작성합니다.
🫢 본론
기존 Exception 은 상황에 맞게 각각 Custom Exception 을 만들어서 처리를 했습니다.
문제점
Exception 마다 각각의 Handler 를 만들어주어야 했습니다.
클래스는 통합해서 작성해도 되지만, 여기서 중요한 건
만든 각각의 Custom Exception 별로 별도의 Handler를 추가로 계속 작성해주어야만 했습니다.
중복된 코드
private static final String MESSAGE = "패스워드가 일치하지 않습니다";
@ExceptionHandler(value = {PasswordMismatchException.class, BadCredentialsException.class})
protected ResponseEntity<ErrorResponseDTO> handleException(RuntimeException e) {
ErrorResponseDTO errorDTO = ErrorResponseDTO.of(e.getClass().getSimpleName(), List.of(MESSAGE));
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorDTO);
}
@ExceptionHandler(value = EmailNotFoundException.class)
protected ResponseEntity<ErrorResponseDTO> handleException(EmailNotFoundException e) {
ErrorResponseDTO errorDTO = ErrorResponseDTO.of(e.getClass().getSimpleName(), List.of(e.getMessage()));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorDTO);
}
거의 모든 코드가 동일하지만, 클라이언트에 내려주어야 하는 Http Status Code 가 다르기 때문입니다. (단지, 그 뿐..)
이 방식을 계속 유지하면서 진행하면, 만든 Exception 에 대해서
Handler를 중복해서 만들게 될 것 같아 리펙토링을 진행했습니다.
리펙토링 진행
ErrorCode 정의
@Getter
@AllArgsConstructor
public enum ErrorCode {
/* 400 BAD_REQUEST : 잘못된 요청 */
MISMATCH_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
TOKEN_TYPE(BAD_REQUEST, "토큰 타입이 올바르지 않습니다."),
UNAVAILABLE_REFRESH_TOKEN(BAD_REQUEST, "사용할 수 없는 토큰 입니다."),
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
EMAIL_NOT_FOUND(NOT_FOUND, "해당 이메일을 찾을 수 없습니다."),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "리프레쉬 토큰을 찾을 수 없습니다."),
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
DUPLICATE_EMAIL(CONFLICT, "이메일이 이미 존재합니다."),
DELETED_EMAIL(CONFLICT, "이미 삭제된 이메일 입니다.");
private final HttpStatus httpStatus;
private final String detail;
}
각각의 Custom Exception 을 만들었던 이유는
어떤 예외인지 Class 명을 보고 빠르게 판단하기 위해
MESSAGE를 내부에서 정의하여 재사용성을 높이기 위해
이렇게 두 가지가 있습니다.
장점을 포함하며, 단점이었던 HttpStatus 를 개별 처리해야하는 걸 enum 으로 한 번에 묶었습니다.
이렇게 진행하게 되면 Custom Exception 을 만든 이유와 HttpStatus 세 개 다 포함할 수 있게 됩니다.
어디서 사용할까?
@Getter
@AllArgsConstructor
public class TicketingException extends RuntimeException {
private final ErrorCode errorCode;
}
Custom Exception 을 공통으로 하나를 만듭니다.
ErrorCode 를 품은 Exception 입니다.
Exception 을 던질 때 TicketingException 을 공통으로 사용하고,
상황에 맞게 ErrorCode를 정의해주는 형식으로 변경했습니다.
Handler 정의
@ExceptionHandler(value = TicketingException.class)
protected ResponseEntity<ErrorResponse> ticketingException(TicketingException ex) {
log.error("TicketingException :: ", ex);
ErrorCode errorCode = ex.getErrorCode();
return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.toErrorResponse(errorCode));
}
짠!
이젠 공통으로 정의한 TicketingException 에 해당하는 하나의 Handler만을 정의해주면 됩니다!
ErrorCode에 status code와 message 가 있기 때문에 불필요하게 handler를 중복해서 만들 필요가 없어졌습니다!
클라이언트에게 통일된 규격으로 내려주기
@Getter
public class ErrorResponse {
private final HttpStatus status;
private final String message;
private final List<String> errors;
public ErrorResponse(HttpStatus status, String message, List<String> errors) {
this.status = status;
this.message = message;
this.errors = errors;
}
public ErrorResponse(HttpStatus status, String message, String error) {
this.status = status;
this.message = message;
this.errors = List.of(error);
}
public static ErrorResponse toErrorResponse(ErrorCode errorCode) {
return new ErrorResponse(
errorCode.getHttpStatus(),
errorCode.name(),
errorCode.getDetail());
}
}
저희는 현재 이렇게 정의했습니다.
- status: httpStatus name
- message: enum name
- errors: enum 에서 정의한 detail 혹은 별도 Exception 에서 받은 messages
Custom Exception 을 통일하긴 했지만, 저희가 만든 Exception 외에도 Spring 에서 만든 Exception 들이 많습니다.
모두 공통으로 정의된 ErrorResponse 를 내려주도록 해보겠습니다.
ResponseEntityExceptionHandler
Spring MVC에서 발생할 수 있는 Exception 을 미리 Handler 로 구현하고,
각각의 Exception 별로 메서드를 구현해놓은 클래스로 우린 해당 클래스를 상속받아 메서드를 재정의하면 됩니다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
/**
* Valid 유효성 검사 실패
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.error("MethodArgumentNotValidException :: ", ex);
List<String> errors = generateErrors(ex);
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors);
return handleExceptionInternal(ex, response, headers, response.getStatus(), request);
}
...
}
이처럼 ResponseEntityExceptionHandler 에 있는 메서드들을 재정의하여
저희가 만든 ErrorResponse 를 만들어서 보내주면 동일한 규격으로 진행할 수 있게 됩니다.
😁 결론
Custom Exception 을 각각 만드는 방식도 좋은 경우가 있습니다.
Exception 클래스 내에서 별도 메서드를 진행한다던지, 메세지를 조합한다던지 등등..
하지만, 제가 지금까지 만든 클래스에서는 단지 Message 만 정의하고 있었고,
ErrorCode로 관리하는 게 더 가독성과 유지보수성이 좋게 느껴졌습니다.
리펙토링 후 장점
- ErrorCode 한 곳에서 관리할 수 있다.
- handler를 개별로 만들 필요가 없어졌다.
- 유지보수성 업 확장성 업!
자세한 코드는 현재 진행 중인 프로젝트에서 확인할 수 있습니다. (링크)
참고
'JVM > Spring' 카테고리의 다른 글
[Spring] Error creating bean with name 'configurationPropertiesBeans' defined in class path resource - Spring Cloud (0) | 2022.07.13 |
---|---|
[Spring] 통합테스트 환경 분리 설정 - Gradle Kotlin DSL (0) | 2022.06.30 |
[Spring] enum으로 @Secured 권한 관리 (0) | 2022.06.18 |
[Spring] @PropertySource yaml 파일 Load 하는 방법 (0) | 2022.06.16 |
[SPRING] 스프링 MVC 구조? (DispatcherServlet?) (0) | 2022.04.22 |