☁️
GlobalExceptionHandler 리팩토링
February 10, 2024
기존 코드
기존 코드의 경우 Handler에 너무 많은 책임이 있었고, 통일되지 않아 지저분한 느낌이 들었다.
package org.devridge.api.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.io.IOException;
@RestControllerAdvice
public class FileExceptionHandler {
@ExceptionHandler(IOException.class)
public ResponseEntity<Void> handleIoException() {
return ResponseEntity.internalServerError().build();
}
}
package org.devridge.api.exception;
import org.devridge.api.exception.member.*;
import org.devridge.common.dto.BaseErrorResponse;
import org.devridge.common.dto.BaseResponse;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.persistence.EntityNotFoundException;
import java.util.NoSuchElementException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<BaseResponse> handleException(NoSuchElementException e) {
return ResponseEntity.badRequest().build();
}
@ExceptionHandler(PasswordNotMatchException.class)
public ResponseEntity<BaseErrorResponse> handleException(PasswordNotMatchException e) {
BaseErrorResponse response = new BaseErrorResponse("password does not match.");
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(SkillsNotValidException.class)
public ResponseEntity<BaseErrorResponse> handleException(SkillsNotValidException e) {
BaseErrorResponse response = new BaseErrorResponse("skills not valid.");
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(WeakPasswordException.class)
public ResponseEntity<BaseErrorResponse> handleException(WeakPasswordException e) {
BaseErrorResponse response = new BaseErrorResponse("weak password.");
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(DuplEmailException.class)
public ResponseEntity<BaseErrorResponse> handleException(DuplEmailException e) {
BaseErrorResponse response = new BaseErrorResponse("email already exists.");
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
}
@ExceptionHandler(DuplNicknameException.class)
public ResponseEntity<BaseErrorResponse> handleException(DuplNicknameException e) {
BaseErrorResponse response = new BaseErrorResponse("nickname already exists.");
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<BaseErrorResponse> handleException(MemberNotFoundException e) {
BaseErrorResponse response = new BaseErrorResponse("member not found");
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<BaseErrorResponse> handleEntityNotFoundException(EntityNotFoundException e) {
BaseErrorResponse response = new BaseErrorResponse("해당 엔티티를 찾을 수 없습니다.");
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<BaseErrorResponse> handleDataIntegrityViolation(DataIntegrityViolationException e) {
BaseErrorResponse response = new BaseErrorResponse("이미 존재하는 데이터입니다.");
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<BaseErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
BaseErrorResponse response = new BaseErrorResponse(e.getMessage());
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
}
@ExceptionHandler(AccessTokenInvalidException.class)
public ResponseEntity<BaseErrorResponse> handleAccessDeniedException(AccessTokenInvalidException e) {
BaseErrorResponse response = new BaseErrorResponse(e.getMessage());
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
}
}
package org.devridge.api.exception;
import org.devridge.common.dto.BaseErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<BaseErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
String errorMessage = String.join(" ", errors);
BaseErrorResponse response = new BaseErrorResponse(errorMessage);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
- 위 코드만 봐도 알 수 있듯이 크기가 클수록 코드의 양이 길어지고 통일성이 사라진다.
- 모든 예외를 전부 처리해야하기 때문에 코드의 양이 길어지며, 확장성 역시 떨어진다.
코드 리팩토링
모든 RuntimeException
을 처리하는 BaseException
을 하나 생성한다.
package org.devridge.api.exception.common;
import lombok.Getter;
@Getter
public class BaseException extends RuntimeException {
int code;
public BaseException(int code, String message) {
super(message);
this.code = code;
}
}
해당 BaseException
을 처리하는 GlobalExceptionHandler
를 생성한다.
package org.devridge.api.exception;
import org.devridge.api.exception.common.BaseException;
import org.devridge.common.dto.BaseErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* developer custom exception
*/
@ExceptionHandler(BaseException.class)
public ResponseEntity<BaseErrorResponse> handleBaseException(BaseException exception) {
return ResponseEntity
.status(exception.getCode())
.body(new BaseErrorResponse(exception.getMessage()));
}
/**
* request valid exception
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<BaseErrorResponse> handleRequestValidException(MethodArgumentNotValidException exception) {
List<String> errors = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
String errorMessage = String.join(" ", errors);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new BaseErrorResponse(errorMessage));
}
/**
* enum validate exception
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<BaseErrorResponse> handleIllegalArgumentException(IllegalArgumentException exception) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new BaseErrorResponse(exception.getMessage()));
}
}
- 위 코드를 통해 필요한 에러만 핸들링하도록 수정함으로써 재사용성을 높였다.
모든 예외처리 클래스가 BaseException
을 상속받도록 하면, 어디서든 statusCode
와 message
를 받을 수 있다.
package org.devridge.api.domain.emailverification.exception;
import org.devridge.api.exception.common.BaseException;
public class EmailVerificationInvalidException extends BaseException {
public EmailVerificationInvalidException(int code, String message) {
super(code, message);
}
}
EmailVerification emailVerification = emailVerificationRepository
.findTopByReceiptEmailOrderByCreatedAtDesc(email)
.orElseThrow(() -> new EmailVerificationInvalidException(404, "해당 데이터를 찾을 수 없습니다."));
위와 같이 처리함으로써 예외처리 코드 및 응답의 통일성, 확장성, GlobalExceptionHandler
의 코드 크기를 줄일 수 있다.
향후 새로운 커스텀 오류를 처리하고 싶은 경우 BaseException
을 상속받아 생성하고, 위 예시와 같이 사용하면 된다.