[SpringBoot] API 오류 응답을 깔끔하게 만드는 CustomException 작성법

cheetah_hong·2025년 11월 19일

SpringBoot

목록 보기
1/1
post-thumbnail

스쳐지나가듯 궁금한 의문점을 곁들인 기본 개념

🤥 IllegalArgumentException으로 시작한 이유와 한계

서비스 로직에서 다음과 같이 IllegalArgumentException을 사용해
예외를 던졌습니다: (오류가 오류지 뭐 라고 생각한 나)

if (userRepository.existsByUsername(dto.getUsername())) {
    throw new IllegalArgumentException("중복된 아이디입니다.");
}

하지만 이 방식은 여러 문제를 만듭니다:

  • 의미가 불명확하다.
    IllegalArgumentException은 너무 일반적인 예외이기 때문에, "중복된
    아이디가 존재함!"이라는 도메인 의미가 제대로 전달되지 않게 됩니다.

  • 구분하기 어렵다.
    IllegalArgumentException이 다양한 곳에서 발생할 경우, 어떤 원인인지
    컨트롤러 혹은 프론트에서 구분하기 힘듭니다.

  • 포멧 일관되지 않다.
    성공 응답은 Response.ok와같은 공통 response로 보내고, 실패는 Spring 기본 에러
    JSON으로 나가는 등 포맷이 달라집니다.


🧐 예외 처리를 왜 해야 하는가?

✔ API 응답의 일관성 유지

모든 API가 동일한 JSON 구조로 응답해야 프론트엔드가 로직을 간단하게 유지할 수 있습니다.

✔ 오류 원인 식별 가능

도메인 단위로 명확한 예외 타입을 정의해두면 원인 식별이 쉬워집니다.

✔ 유지보수성 향상

예외를 enum/ErrorCode로 관리하면 에러 종류를 한 곳에서 관리할 수 있습니다.


1️⃣ 보편적인 예외 처리 구조

✔ (1) CustomException + ErrorCode 생성

ErrorCode:

public enum ErrorCode {
    DUPLICATE_USERNAME(HttpStatus.CONFLICT, "DUPLICATE_USERNAME", "이미 존재하는 아이디입니다."),
    INVALID_REQUEST(HttpStatus.BAD_REQUEST, "INVALID_REQUEST", "잘못된 요청입니다."),
    INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "서버 오류");
}

각 Enum타입의 정보에는:

  • HttpStatus → HTTP 프로토콜 기준 상태 코드
  • code → 프론트에서 분기하기 위한 시스템 코드
  • message → 사용자에게 보여줄 문구

🧑‍💻 codeHttpStatus(400,409등) 큰 범주 안에서 상세 내용이라고 생각.
Ex) 409 하위에는 DUPLICATE_ID, DUPLICATE_NICKNAME, PASSWORD_UNCHANGED

CustomException:

public class CustomException extends RuntimeException {
    private final ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

서비스에서 아래와 같이 예외를 던집니다

if (userRepository.existsByUsername(dto.getUsername())) {
    throw new CustomException(ErrorCode.DUPLICATE_USERNAME);
}

✔ (2) Global Exception Handler

@RestControllerAdvice로 전역 예외 처리

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<CommonResponse> handleCustomException(CustomException ex) {
        ErrorCode error = ex.getErrorCode();
        return ResponseEntity
                .status(error.getStatus())
                .body(CommonResponse.error(error.getMessage(), error.getCode()));
    }
}

이제 오류가 발생하면 항상 같은 JSON 포맷을 반환합니다.


2️⃣ CommonResponse와의 통합

성공:

{
  "success": true,
  "message": "회원가입 완료",
  "code": null,
  "data": {...}
}

실패:

{
  "success": false,
  "message": "이미 존재하는 아이디입니다.",
  "code": "DUPLICATE_USERNAME",
  "data": null
}

✅ (결론) 예외 처리 구조의 이점

✔ 응답 포맷 통일

성공/실패 모두 같은 JSON 구조 → 프론트 개발이 쉬워짐

✔ 비즈니스 오류 구분 확실

DUPLICATE_USERNAME, INVALID_REQUEST 등 도메인 기준 오류 파악 가능

✔ 유지보수와 확장성 증가

ErrorCode에 정의만 추가하면 API 전체에서 바로 사용 가능

✔ HTTP status + 내부 코드(code) 동시 제공

서버는 HTTP status로, 클라이언트는 code로 오류 판단 가능

✔ 로깅 및 모니터링이 용이

어떤 오류가 얼마나 발생했는지 ErrorCode 기반으로 추적 가능


🐆🐆🐆 성공시 code가 필수로 null로 나오는 것이 안 불편한가? (그냥 나의 생각) 🐆🐆🐆

  • 성공은 대부분 한 종류 → 추가 code 무의미 (fail case대로 늘어나면 적어도 두배..?)
  • 프론트는 실패에서 code로 분기하고 성공 응답은 message와 data만 있으면 충분.
  • 안 쓰는 변수 계속 출력하는 느낌으로 불편함 느낌 뭔가 뭔가

답을 아시는 분 알려주십쇼🙇‍♂️

profile
치타는 지금 웃고있다 🐆

1개의 댓글

comment-user-thumbnail
2026년 3월 26일

자네..!! 스프링부트를 참 잘하는구만

답글 달기