runtime-role-smoke-20260714.../docs/adr/ADR-002-error-contract.md

3.7 KiB

ADR-002: 오류 계약(Error Contract) 정의

Context

REST API에서 일관된 오류 응답 형식을 제공하여:

  • 클라이언트가 오류를 명확히 이해 가능
  • API 버전 간 호환성 유지
  • 디버깅 및 모니터링 용이성 확보

를 목적으로 한다.

Decision

1. 오류 응답 표준 형식

{
  "timestamp": "2026-07-14T05:00:00Z",
  "status": 400,
  "error": "Bad Request",
  "code": "ROLE_001",
  "message": "역할 이름은 필수입니다",
  "path": "/api/v1/roles",
  "details": [
    {
      "field": "name",
      "rejectedValue": "",
      "message": "must not be blank"
    }
  ]
}

2. 오류 코드 체계

Prefix 범위 설명
ROLE_ 001-099 역할 관련 오류
AUTH_ 100-199 인증/인가 오류
VAL_ 900-949 검증 오류
SYS_ 950-999 시스템 오류

3. HTTP 상태 코드 매핑

상태 코드 사용 시점
400 Bad Request 입력 검증 실패
401 Unauthorized 인증 실패
403 Forbidden 권한 없음
404 Not Found 리소스 존재하지 않음
409 Conflict 리소스 충돌 (중복 등)
500 Internal Server Error 예상치 못한 서버 오류

4. 예외 클래스 계층 구조

BaseException (abstract)
├── BusinessException
│   ├── RoleNotFoundException (ROLE_001)
│   ├── RoleAlreadyExistsException (ROLE_002)
│   └── UnauthorizedAccessException (AUTH_001)
├── ValidationException (VAL_001)
└── SystemException (SYS_001)

5. 구현 클래스

// BaseException.java
public abstract class BaseException extends RuntimeException {
    private final String errorCode;
    private final HttpStatus httpStatus;
    
    protected BaseException(String errorCode, HttpStatus httpStatus, String message) {
        super(message);
        this.errorCode = errorCode;
        this.httpStatus = httpStatus;
    }
}

// ErrorResponse.java
public record ErrorResponse(
    Instant timestamp,
    int status,
    String error,
    String code,
    String message,
    String path,
    List<FieldError> details
) {
    public record FieldError(String field, Object rejectedValue, String message) {}
}

// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex, HttpServletRequest request) {
        ErrorResponse response = ErrorResponse.of(ex, request.getRequestURI());
        return ResponseEntity.status(ex.getHttpStatus()).body(response);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex, HttpServletRequest request) {
        ErrorResponse response = ErrorResponse.ofValidation(ex, request.getRequestURI());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
}

Alternatives

대안 1: RFC 7807 Problem Details

  • application/problem+json Content-Type 사용
  • 단점: 클라이언트 라이브러리 지원 제한적
  • 채택하지 않음 (일반 JSON 응답 채택)

대안 2: 단순 오류 메시지만 반환

  • 단점: 오류 코드 부재로 클라이언트 처리 어려움
  • 채택하지 않음

Consequences

Positive:

  • 일관된 API 응답으로 클라이언트 개발 편의성 향상
  • 오류 코드 기반 로컬라이제이션 가능
  • 모니터링 시스템 연동 용이

Negative:

  • 오류 응답 클래스 추가 작성 필요
  • 기존 예외 처리 코드 마이그레이션 필요