3) 에러 응답 설계
3-1) ErrorResponse DTO
@Getter
@Builder
public class ErrorResponse {
private String errorCode; // 에러 코드 (고유 식별자)
private String message; // 사용자용 메시지
private LocalDateTime timestamp; // 발생 시각
private String path; // 요청 경로
// 선택적
private List<FieldError> fieldErrors; // Validation 에러 상세
}
@Getter
@AllArgsConstructor
public class FieldError {
private String field;
private String value;
private String reason;
}
3-2) 에러 코드 관리
public enum ErrorCode {
// 4xx Client Errors
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "사용자를 찾을 수 없습니다."),
ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "O001", "주문을 찾을 수 없습니다."),
INVALID_INPUT(HttpStatus.BAD_REQUEST, "C001", "입력값이 올바르지 않습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "A001", "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "A002", "권한이 없습니다."),
// 5xx Server Errors
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S001", "서버 오류가 발생했습니다."),
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S002", "데이터베이스 오류가 발생했습니다.");
private final HttpStatus status;
private final String code;
private final String message;
ErrorCode(HttpStatus status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
public HttpStatus getStatus() { return status; }
public String getCode() { return code; }
public String getMessage() { return 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;
}
}
// 비즈니스 로직에서
throw new BusinessException(ErrorCode.USER_NOT_FOUND, "사용자 ID: " + userId);
4) 커스텀 예외 계층
4-1) 예외 계층 설계
// 최상위 비즈니스 예외
public abstract class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
protected BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
protected BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
// 도메인별 예외
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(Long userId) {
super(ErrorCode.USER_NOT_FOUND, "사용자를 찾을 수 없습니다: ID=" + userId);
}
}
public class OrderNotFoundException extends BusinessException {
public OrderNotFoundException(Long orderId) {
super(ErrorCode.ORDER_NOT_FOUND, "주문을 찾을 수 없습니다: ID=" + orderId);
}
}
public class InvalidOrderStateException extends BusinessException {
public InvalidOrderStateException(String currentState, String expectedState) {
super(ErrorCode.INVALID_INPUT,
String.format("주문 상태가 올바르지 않습니다. 현재: %s, 기대: %s", currentState, expectedState));
}
}
4-2) 전역 예외 핸들러
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 비즈니스 예외 통합 처리
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException e,
HttpServletRequest request) {
log.warn("Business exception: {} - {}", e.getErrorCode().getCode(), e.getMessage());
ErrorResponse error = ErrorResponse.builder()
.errorCode(e.getErrorCode().getCode())
.message(e.getMessage())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity
.status(e.getErrorCode().getStatus())
.body(error);
}
// Validation 예외
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException e,
HttpServletRequest request) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors().stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() != null ? error.getRejectedValue().toString() : null,
error.getDefaultMessage()
))
.collect(Collectors.toList());
ErrorResponse error = ErrorResponse.builder()
.errorCode("VALIDATION_FAILED")
.message("입력값 검증에 실패했습니다.")
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.fieldErrors(fieldErrors)
.build();
return ResponseEntity.badRequest().body(error);
}
// 인증 예외
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(
AccessDeniedException e,
HttpServletRequest request) {
log.warn("Access denied: {}", request.getRequestURI());
ErrorResponse error = ErrorResponse.builder()
.errorCode("ACCESS_DENIED")
.message("접근 권한이 없습니다.")
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
// 모든 예외 (마지막 방어선)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(
Exception e,
HttpServletRequest request) {
log.error("Unexpected error occurred", e);
ErrorResponse error = ErrorResponse.builder()
.errorCode("INTERNAL_SERVER_ERROR")
.message("서버 오류가 발생했습니다.")
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
📚 다음 편: 준비 중입니다.
💬 댓글