개요

API 에러 규격의 핵심은 에러를 안정적으로 식별할 수 있는 Code 체계 확보임 HTTP Status만으로는 부족하고 메시지는 언어·맥락에 따라 바뀔 수 있음 실제로 계약으로서 신뢰할 수 있는 값은 error.code 임 아래는 다수의 글로벌 서비스에서 공통적으로 쓰는 Error Code 설계 원칙 정리

설계 원칙

  • error.code 는 서비스 전반의 안정적인 식별자여야 함
  • 메시지나 HTTP Status는 변경 가능하지만 error.code 는 변경 불가
  • 구버전 클라이언트도 동일 코드를 신뢰해야 하므로 호환성 보장 필수
  • 숫자형 대신 의미가 드러나는 문자열 기반 Enum 권장
    • 숫자형은 의미 파악 어려움, 매뉴얼 의존, 협업 비용 증가
    • 문자열 Enum은 가독성, 검색성, 커뮤니케이션 효율 우수

예시

"USER_ALREADY_EXISTS"
"INVALID_COUPON"
"PAYMENT_INSUFFICIENT_BALANCE"

네이밍 규칙

  • 대문자 + 언더스코어 형태 CONSTANT_CASE 권장
    • 언어/플랫폼 비종속, 문서화 시 표현 깔끔함
  • 도메인 + 문제 유형 조합 권장
    • 패턴: {DOMAIN}_{CONDITION}

예시

  • USER_NOT_FOUND 해당 유저를 찾을 수 없음
  • USER_ALREADY_EXISTS 이미 가입된 유저
  • AUTH_TOKEN_EXPIRED 인증 토큰 만료
  • ORDER_NOT_CANCELABLE 현재 상태에서는 주문 취소 불가
  • PAYMENT_INSUFFICIENT_BALANCE 잔액 부족
  • FILE_TOO_LARGE 허용 파일 크기 초과

카테고리 분리 전략

A. Prefix로 도메인 구분 권장

  • AUTH_, USER_, ORDER_, PAYMENT_, COMMON_, FILE_ 등으로 그룹화
  • 코드만 보고도 영역 식별 가능, 문서 구조화 용이

B. HTTP Status와 비즈니스 코드는 분리

  • 400_USER_INVALID_EMAIL 같은 결합은 지양
  • HTTP Status는 정책·맥락에 따라 변할 수 있으나 code는 절대 불변이어야 함
  • 원칙 HTTP Status와 Error Code는 결합하지 않음

ErrorCode Enum 예시

NestJS/TypeScript 기준 예시

export enum ErrorCode {
  // ===== 공통 =====
  INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
  INVALID_INPUT = 'INVALID_INPUT',
  VALIDATION_FAILED = 'VALIDATION_FAILED',
  RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',

  // ===== Auth =====
  AUTH_REQUIRED = 'AUTH_REQUIRED',
  AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED',
  AUTH_INVALID_TOKEN = 'AUTH_INVALID_TOKEN',

  // ===== User =====
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
  USER_EMAIL_NOT_VERIFIED = 'USER_EMAIL_NOT_VERIFIED',

  // ===== Order =====
  ORDER_NOT_FOUND = 'ORDER_NOT_FOUND',
  ORDER_NOT_CANCELABLE = 'ORDER_NOT_CANCELABLE',

  // ===== Payment =====
  PAYMENT_INSUFFICIENT_BALANCE = 'PAYMENT_INSUFFICIENT_BALANCE',
  PAYMENT_METHOD_INVALID = 'PAYMENT_METHOD_INVALID',

  // ===== File Upload =====
  FILE_TOO_LARGE = 'FILE_TOO_LARGE',
  FILE_UNSUPPORTED_TYPE = 'FILE_UNSUPPORTED_TYPE',
}

이 구성의 효과

  • 프론트·앱에서 조건 분기 용이
  • 의미 중심 네이밍으로 직관성 확보
  • 도메인 확장에 따른 추가가 간단
  • 유지보수 비용 낮음

안티 패턴

  • 숫자 코드만 사용
    • 예시 “E4302” 의미 불명확, 협업 비용 증가
  • HTTP Status를 code에 포함
    • 예시 “400_INVALID_EMAIL” Status 변경 시 code 불변 원칙과 충돌
  • 지나치게 포괄적 네이밍
    • BAD_REQUEST, CONFLICT 등은 비즈니스 의미 전달 부족
  • 전역 중복 이름
    • NOT_FOUND 단일 정의는 도메인 식별 불가
  • 메시지 문구를 code에 포함
    • 예시 “EMAIL_HAS_INVALID_FORMAT” 미묘한 정책 변경 시 관리 비용 증가, 번역·UX 변경 어려움

코드·메시지·HTTP Status 매핑

명확한 맵핑 구조를 두면 운영과 문서화가 단순해짐

export const ErrorMeta: Record<ErrorCode, { httpStatus: number; message: string }> = {
  INTERNAL_SERVER_ERROR: { httpStatus: 500, message: '서버 내부 오류가 발생했습니다.' },
  INVALID_INPUT: { httpStatus: 400, message: '입력값이 잘못되었습니다.' },
  VALIDATION_FAILED: { httpStatus: 400, message: '입력값 검증에 실패했습니다.' },
  RESOURCE_NOT_FOUND: { httpStatus: 404, message: '리소스를 찾을 수 없습니다.' },

  AUTH_REQUIRED: { httpStatus: 401, message: '인증이 필요합니다.' },
  AUTH_TOKEN_EXPIRED: { httpStatus: 401, message: '인증 토큰이 만료되었습니다.' },
  AUTH_INVALID_TOKEN: { httpStatus: 401, message: '잘못된 인증 토큰입니다.' },

  USER_NOT_FOUND: { httpStatus: 404, message: '사용자를 찾을 수 없습니다.' },
  USER_ALREADY_EXISTS: { httpStatus: 409, message: '이미 가입된 유저입니다.' },
  USER_EMAIL_NOT_VERIFIED: { httpStatus: 403, message: '이메일 인증이 필요합니다.' },

  ORDER_NOT_FOUND: { httpStatus: 404, message: '주문을 찾을 수 없습니다.' },
  ORDER_NOT_CANCELABLE: { httpStatus: 409, message: '현재 주문은 취소할 수 없습니다.' },

  PAYMENT_INSUFFICIENT_BALANCE: {
    httpStatus: 402,
    message: '잔액이 부족합니다.',
  },
  PAYMENT_METHOD_INVALID: {
    httpStatus: 400,
    message: '결제 수단이 유효하지 않습니다.',
  },

  FILE_TOO_LARGE: { httpStatus: 400, message: '파일이 너무 큽니다.' },
  FILE_UNSUPPORTED_TYPE: { httpStatus: 400, message: '지원하지 않는 파일 형식입니다.' },
};

전역 예외 필터 예시 사용법

const meta = ErrorMeta[exception.code];
return res.status(meta.httpStatus).json({
  error: {
    code: exception.code,
    httpStatus: meta.httpStatus,
    message: meta.message,
    details: exception.details ?? null,
    traceId,
    timestamp,
  }
});

요점

  • ErrorCode는 비즈니스 의미 유지
  • HTTP Status, 메시지는 환경·정책에 맞게 별도 관리
  • 관측성 지표로 traceId 등 메타데이터 포함 권장

도입 체크리스트

  • Code가 불변 계약인지 확인 API 계약 준수
  • Code만 보고 의미 파악 가능한지 확인 YES 기준
  • 모든 코드에 고유 도메인 접두가 있는지 확인 USER, AUTH 등
  • HTTP Status와 code 결합 금지 여부 확인 결합 금지
  • 메시지와 code의 독립성 확인 번역·UX 변경 용이성
  • Validation 오류 코드 일관성 확인 VALIDATION_FAILED 등 공용 코드 사용
  • 로그에서 traceId 기반 추적 가능 여부 확인 운영 디버깅 용이성

마무리

좋은 Error Code 체계는 프론트·앱·백엔드 간 계약이자 디버깅·모니터링·UX 일관성을 좌우하는 핵심 설계 요소

  • code의 안정성 확보
  • 비즈니스 의미 중심의 도메인 기반 네이밍
  • Status와 메시지의 분리된 관리
  • 팀 합의 컨벤션으로 문서화와 운영 일관성 유지 이 네 가지를 충족하면 확장성과 유지보수성이 뛰어난 API 에러 설계로 평가 가능

참고자료