Project/Boilerplate

API 예외 처리

조용우 2025. 3. 17. 17:00

기존 예외 처리 문제점

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를 매번 추가할 필요 없음