기존 예외 처리 문제점
IllegalArugmentException, IllegalStateException 등 혼용해서 쓰다 보니 헷갈림.
IllegalArgumentException(UserError.USER_NOT_FOUND.getMessage()) 와 같은 방식으로 메세지를 전달하기 때문에 코드 중복이 많아짐.
RestControllerAdvice에서 전역적으로 관리할 때, 새로운 클래스의 에러가 추가될 때 마다 계속해서 추가해주어야 함.
해결:
에러 코드와 메세지를 관리할 ErrorCode 인터페이스
public interface ErrorCode {
String name();
HttpStatus getStatus();
String getMessage();
}
도메인 별 에러코드
@Getter
@RequiredArgsConstructor
public enum GlobalError implements ErrorCode {
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러가 발생했습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "접근 권한이 없습니다.");
private final HttpStatus status;
private final String message;
}
@Getter
@RequiredArgsConstructor
public enum UserError implements ErrorCode {
DUPLICATE_USER(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"),
USER_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "유효한 인증 정보가 없습니다");
private final HttpStatus status;
private final String message;
}
등등
모든 커스텀 예외가 상속받을 비즈니스 예외 클래스
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
생성자1: enum의 기본 에러 메세지
생성자2: 추가적으로 상세한 에러메세지 필요할 때 ex) new EmailException("Email" + email + "이 잘못되었습니다")
커스텀 예외들은 BusinessException을 상속받아 구현
public class UserNotFoundException extends BusinessException {
public UserNotFoundException() {
super(UserError.USER_NOT_FOUND);
}
}
Response로 응답할 ErrorResponse 클래스
@Getter
@Builder
public class ErrorResponse {
private final String message;
private final int status;
private final String code;
private final LocalDateTime timestamp;
private final Map<String, Object> details;
public static ErrorResponse of(BusinessException e) {
return ErrorResponse.builder()
.message(e.getMessage())
.status(e.getErrorCode().getStatus().value())
.code(e.getErrorCode().name())
.timestamp(LocalDateTime.now())
.details(new HashMap<>())
.build();
}
public static ErrorResponse of(ErrorCode errorCode) {
return ErrorResponse.builder()
.message(errorCode.getMessage())
.status(errorCode.getStatus().value())
.code(errorCode.name())
.timestamp(LocalDateTime.now())
.details(new HashMap<>())
.build();
}
public void addDetail(String key, Object value) {
this.details.put(key, value);
}
}
public void deletePost(Long postId, Long userId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> {
BusinessException ex = new PostNotFoundException();
ErrorResponse response = ErrorResponse.of(ex);
response.addDetail("postId", postId);
response.addDetail("requestedBy", userId);
return ex;
});
}
Details의 경우 위와 같은 방식으로 추가 데이터를 넣을 수 있음
GlobalExceptionHandler에서 에러 전역적으로 처리
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// BusinessException 하위 모든 예외 처리
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.error("BusinessException: {}", e.getMessage());
ErrorResponse response = ErrorResponse.of(e);
return new ResponseEntity<>(response, e.getErrorCode().getStatus());
}
// 처리되지 않은 모든 예외 처리
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unhandled Exception: ", e);
ErrorResponse response = ErrorResponse.of(GlobalError.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
효과:
새로운 예외 추가가 쉬워지고, 에러 응답 형식이 일관됨
에러코드와 메세지를 도메인별로 한 곳에서 관리가 가능하고
ExceptionHandler를 매번 추가할 필요 없음
'Project > Boilerplate' 카테고리의 다른 글
| WebMvcTest + Spring REST doc 적용 ( 1 / 2 - MockMvc ) (0) | 2025.03.28 |
|---|---|
| 필터 예외 처리 (0) | 2025.03.17 |
| @PreAuthorize를 활용한 댓글 수정/삭제 권한 관리 및 코드 개선 (0) | 2025.03.10 |
| Cursor 페이지네이션에서의 정렬, 필터링 (0) | 2025.03.10 |
| 게시글 Pagination 적용 (2 / 2 - 인피니티 스크롤형 게시판) (0) | 2025.03.10 |