작년에 프로젝트를 진행할 때는 각 서비스와 컨트롤러 단에서 if로 체크해서 예외처리를 해줬다. 불편하다고 생각하고는 있었는데 전체를 다른 방식으로 바꿀 정도로 불편한건 아닌거 같아서 그대로 사용하다가 올해 프로젝트를 새로 하면서 전역 예외 처리를 사용하면서 이 방식에 대해 알게되었고, 지금 새로 시작한 프로젝트에서 전역 예외 처리를 구현해서 티스토리에 정리한다.
체크 예외, 언체크 예외
체크 예외
컴파일러가 처리하는 RuntimeException을 상속받지 않는 예외로 이를 처리하지 않으면 CompileError가 발생한다 (IOException, SQLException)
언체크 예외
RuntimeException을 상속받는 예외로 예외처리가 필수는 아니며 Transaction 롤백 대상이다. (비즈니스 예외는 언체크 예외로 구현하는 것이 좋음)
전역으로 공통 예외처리
ErrorCode
httpStatus 값과 에러코드, 에러 메시지를 포함한다. 이전에 status Code만 사용하니까 정리가 덜 되는 듯한 느낌이어서 자체 코드를 만들어서 코드와 status를 분리했다.
@AllArgsConstructor
@Getter
public enum ErrorCode {
//common
NOT_FOUND_DATA(404, "C001", "해당하는 데이터를 찾을 수 없습니다."),
BAD_REQUEST(400, "C002", "잘못된 요청입니다.");
private final Integer status;
private final String code;
private final String message;
}
ErrorResponse
자체 코드, 메시지, timestamp, trackingId, detailMessage를 가진다. 처음에는 httpStatus 값도 함께 body에 넣을까 했는데 어짜피 header의 상태 코드에 들어가는 값이기 때문에 제외했다.
@Getter
public class ErrorResponse {
private final String code;
private final String message;
private final String timestamp;
private final String trackingId;
private final String detailMessage;
public ErrorResponse(ErrorCode code, String detailMessage) {
this.code = code.getCode();
this.message = code.getMessage();
this.timestamp = LocalDateTime.now().toString();
this.trackingId = UUID.randomUUID().toString();
this.detailMessage = detailMessage;
}
public ErrorResponse(ErrorCode code) {
this.code = code.getCode();
this.message = code.getMessage();
this.timestamp = LocalDateTime.now().toString();
this.trackingId = UUID.randomUUID().toString();
this.detailMessage = "";
}
public static ResponseEntity<ErrorResponse> of(ErrorCode code) {
return ResponseEntity
.status(HttpStatus.valueOf(code.getStatus()))
.body(new ErrorResponse(code));
}
public static ResponseEntity<ErrorResponse> of(ErrorCode code, String detailMessage) {
return ResponseEntity
.status(HttpStatus.valueOf(code.getStatus()))
.body(new ErrorResponse(code, detailMessage));
}
}
BusinessException
RuntimeException을 상속받는 내 프로젝트의 최상위 Exception이다. 앞으로 생성되는 모든 하위 BusinessException은 BusinessException을 상속받게 구현한다.
@Getter
public class BusinessException extends RuntimeException{
private ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException( ErrorCode errorCode, String detailMessage) {
super(detailMessage);
this.errorCode = errorCode;
}
}
GlobalExceptionController
@RestControllerAdvice를 사용해 전역 예외처리를 할 수 있도록 했다. @ExceptionHandler를 이용해 각 Exception마다 메소드를 만들어 어떻게 예외처리를 할지 지정해 줬다. 지금은 BusinessException에 대해서만 설정해 줬지만 추후 [ MethodArgumentNotValidException,BindException, MethodArgumentTypeMismatchException ] 등과 같은 여러 기본 exception에 대해서도 처리해 줄 예정이다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
protected ResponseEntity<ErrorResponse> businessExceptionHandler(BusinessException e){
return ErrorResponse.of(e.getErrorCode(), e.getMessage());
}
}