REST API에서 일관된 에러 바디와 예외 처리 패턴을 정의하는 과정을 시각화한 개념도입니다.
REST API 8회차 : 에러 설계와 예외 처리 – 에러 바디 표준, Validation 패턴, 에러 응답 템플릿 실습
한눈에 보는 요약
REST API에서 “에러 응답”은 성공 응답만큼이나 중요합니다. 에러 설계가 잘 되어 있으면 클라이언트 개발자가 문제를 빠르게 진단하고, 사용자에게도 일관된 에러 메시지를 제공할 수 있습니다. 반대로 API마다 제각각인 에러 구조는 디버깅 비용을 크게 증가시킵니다.
본 포스팅에서는 에러 바디에 포함해야 할 핵심 필드(코드, 메시지, 상세, 추적 ID)를 표준화하는 방법과, 검증(Validation) 에러를 표현하는 대표적인 패턴을 설명합니다. 또한 실습 형태로 “에러 응답 템플릿”을 만들어 보면서 실제 프로젝트에 바로 적용할 수 있는 구조를 제안합니다.
언어나 프레임워크(Spring, Node.js 등)에 상관없이 공통으로 활용할 수 있는 개념 위주로 설명하고, 코드 예시는 이해를 돕기 위한 참고용으로 보시면 됩니다.
목차
- 1. 핵심 포인트
- 2. 에러 설계와 예외 처리가 중요한 이유
- 3. 에러 바디 표준 구조 설계 (코드/메시지/상세/추적ID)
- 4. 검증(Validation) 에러 패턴
- 5. 실습: 에러 응답 템플릿 만들기
- 6. 코드 예시
- 7. 에러 바디 필드 설계 비교 표
- 8. 적용 단계 정리
- 9. 추가로 생각해볼 점
- 10. 블로그 최적화 정보
핵심 포인트
- 에러 응답은 “일관된 구조”가 가장 중요하며, 최소한 HTTP 상태코드 + 비즈니스 에러코드 + 메시지 + 추적ID를 함께 설계하는 것이 좋습니다.
- 에러 메시지는 사람을 위한 자연어, 에러 코드는 시스템을 위한 식별자 역할을 하므로, 둘을 명확히 구분해서 사용해야 유지보수가 쉬워집니다.
- Validation 에러는 단일 메시지로 뭉개지기보다, 필드별 오류 리스트를 내려주는 패턴이 디버깅과 UX 측면에서 모두 유리합니다.
- 실무에서는 예외를 직접 컨트롤러마다 처리하기보다, 전역 예외 처리 레이어(예: @RestControllerAdvice)에서 에러 응답 템플릿으로 변환하도록 설계하는 것이 일반적입니다.
- 추적ID(traceId, requestId)를 반드시 에러 바디에 포함해서, 서버 로그와 클라이언트 에러를 한 번에 연결할 수 있도록 하는 것이 좋습니다.
상세 설명
1. 에러 설계와 예외 처리가 중요한 이유
REST API에서 에러 응답은 단순히 “문제가 발생했습니다”라고 알려주는 수준을 넘어, 문제를 빠르게 해결하기 위한 정보를 전달하는 역할을 합니다. 특히 마이크로서비스나 모바일/웹 클라이언트가 여러 개 존재하는 환경에서는, 에러 구조가 표준화되어 있지 않으면 팀 간 커뮤니케이션 비용과 디버깅 시간이 크게 늘어납니다.
또한 API가 외부 파트너나 공개 API로 사용되는 경우, 에러 응답은 사실상 “계약(Contract)”입니다. 한 번 정의된 구조를 함부로 바꾸기 어렵기 때문에, 초기에 어느 정도 잘 설계해 두면 이후 변경 비용을 크게 줄일 수 있습니다. 따라서 에러 설계는 API 설계의 필수 요소로 보시는 것이 좋습니다.
2. 에러 바디 표준 구조 설계 (코드/메시지/상세/추적ID)
에러 바디는 성공 응답과 마찬가지로, 팀에서 합의된 공통 포맷을 사용하는 것이 핵심입니다. 일반적으로 다음과 같은 필드를 많이 사용합니다.
- status: HTTP 상태코드(숫자) – 400, 404, 500 등
- code: 시스템 내부에서 사용하는 비즈니스 에러 코드 – 문자열 또는 숫자
- message: 최종 사용자나 클라이언트 개발자를 위한 사람 친화적인 메시지
- detail: 디버깅을 돕는 추가 정보(옵션) – 내부 사유, 파라미터, 힌트 등
- traceId: 로그와 연동하기 위한 요청 단위의 추적 ID
- errors: Validation 에러 등 필드별 상세 오류 리스트(옵션)
간단한 예시는 다음과 같습니다.
{ "status": 400, "code": "USER_INVALID_INPUT", "message": "요청 값이 올바르지 않습니다.", "detail": "email 형식이 잘못되었습니다.", "traceId": "9f7c3a0e-0f2b-4f60-8bda-6c93e4f11234" } 위 예시에서 status는 HTTP 응답 코드와 동일하게 유지하고, code는 비즈니스 로직에서 사용하는 자체 코드입니다. 메시지는 사용자에게 그대로 노출될 수 있다는 점을 고려해, 너무 기술적이지 않게 작성하되, detail에는 보다 기술적인 설명이나 내부용 정보를 포함할 수 있습니다. traceId는 서버 로그에 함께 남겨두면, 운영 이슈 발생 시 에러 바디만 보고도 관련 로그를 빠르게 찾을 수 있습니다.
3. 검증(Validation) 에러 패턴
입력 값 검증(Validation) 에러는 일반 비즈니스 에러와 구조가 약간 다릅니다. 한 번에 여러 필드에서 동시에 에러가 발생할 수 있기 때문에, 보통 errors라는 배열에 필드별 에러를 담는 패턴을 많이 사용합니다.
대표적인 구조는 다음과 같습니다.
{ "status": 400, "code": "VALIDATION_FAILED", "message": "요청 데이터가 유효하지 않습니다.", "traceId": "a1234567-b890-4cde-8123-4567890abcde", "errors": [ { "field": "email", "code": "INVALID_EMAIL", "message": "이메일 형식이 올바르지 않습니다." }, { "field": "age", "code": "MIN", "message": "나이는 18 이상이어야 합니다." } ] } 이 패턴의 장점은 다음과 같습니다.
- 클라이언트(웹/앱)가 각 필드별로 메시지를 바로 UI에 매핑할 수 있습니다.
- 한 번의 요청으로 여러 개의 오류를 동시에 사용자에게 알려, 재시도 횟수를 줄여 줍니다.
- 에러 코드(
code)를 활용해 클라이언트에서 메시지를 현지화(다국어 처리)하거나, 특정 오류에 대한 자동 처리 로직을 넣을 수 있습니다.
필드명이 없는 “글로벌 에러”(예: 두 필드 간의 조합 오류)가 필요한 경우, field를 null 또는 빈 문자열로 두거나 object, global과 같은 특별한 값을 사용하는 방식도 있습니다. 팀 내에서 하나의 규칙만 선택해 일관되게 사용하는 것이 중요합니다.
4. 실습: 에러 응답 템플릿 만들기
이제 위에서 설명한 내용을 바탕으로, 실무에서 사용할 수 있는 “에러 응답 템플릿”을 설계해 보겠습니다. 우선 공통 포맷을 정의하고, 이후 각 예외 상황에서 이 포맷을 채워 사용하는 방식입니다.
먼저 공통 에러 응답 템플릿을 JSON 형태로 설계해 봅니다.
{ "status": <number>, "code": "<string>", "message": "<string>", "detail": "<string (optional)>", "traceId": "<string>", "errors": [ { "field": "<string>", "code": "<string>", "message": "<string>" } ] } 언어나 프레임워크에 따라 이 템플릿을 클래스로 만들 수 있습니다. 예를 들어 Java/Spring 환경이라면, ErrorResponse, ValidationError 같은 DTO 클래스를 만들고, 전역 예외 처리기에서 이 DTO를 채워 반환하면 됩니다. Node.js/Express라면 미들웨어에서 위 구조로 JSON을 구성해 res.status().json()으로 내려주면 됩니다.
코드 예시
아래는 Spring Boot 환경에서 전역 예외 처리기로 에러 응답 템플릿을 사용하는 간단한 예시입니다. (개념 이해용 코드이며, 프로젝트 상황에 맞게 수정이 필요합니다.)
@Data @Builder public class ErrorResponse { private int status; private String code; private String message; private String detail; private String traceId; private List<FieldError> errors; @Data @Builder public static class FieldError { private String field; private String code; private String message; } } 위 ErrorResponse 클래스는 공통 에러 응답 템플릿을 Java 코드로 표현한 예시입니다. 필드별 오류를 담기 위한 내부 클래스 FieldError를 포함하고 있어 Validation 에러도 같은 포맷으로 표현할 수 있습니다.
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { String traceId = (String) request.getAttribute("traceId"); // 필터에서 세팅해 두었다고 가정 List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult().getFieldErrors() .stream() .map(error -> ErrorResponse.FieldError.builder() .field(error.getField()) .code(error.getCode()) .message(error.getDefaultMessage()) .build()) .toList(); ErrorResponse body = ErrorResponse.builder() .status(HttpStatus.BAD_REQUEST.value()) .code("VALIDATION_FAILED") .message("요청 데이터가 유효하지 않습니다.") .traceId(traceId) .errors(fieldErrors) .build(); return ResponseEntity.badRequest().body(body); } @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusiness(BusinessException ex, HttpServletRequest request) { String traceId = (String) request.getAttribute("traceId"); ErrorResponse body = ErrorResponse.builder() .status(ex.getHttpStatus().value()) .code(ex.getErrorCode()) .message(ex.getMessage()) .detail(ex.getDetail()) .traceId(traceId) .build(); return ResponseEntity.status(ex.getHttpStatus()).body(body); } } 위 예시에서는 Validation 에러(MethodArgumentNotValidException)와 비즈니스 에러(BusinessException)를 각각 처리하여, 앞에서 정의한 공통 에러 응답 템플릿으로 변환하고 있습니다. traceId는 필터나 인터셉터에서 생성해 요청 스코프에 담아두었다가, 예외 처리기에서 꺼내어 함께 내려주는 방식입니다.
에러 바디 필드 설계 비교 표
아래 표는 에러 바디에 포함할 수 있는 대표 필드와, 각 필드의 목적 및 사용 예시를 정리한 것입니다.
| 필드명 | 타입 | 필수 여부 | 역할/설명 | 예시 값 |
|---|---|---|---|---|
| status | number | 필수 | HTTP 상태코드. 클라이언트가 에러 타입(4xx/5xx)을 구분하는 기준이 됩니다. | 400, 404, 500 |
| code | string | 필수 | 비즈니스 에러 코드. 로그 검색 및 클라이언트 분기 처리에 사용합니다. | USER_NOT_FOUND, VALIDATION_FAILED |
| message | string | 필수 | 사람이 읽을 수 있는 에러 설명. 사용자에게 직접 노출될 수 있습니다. | “요청 값이 올바르지 않습니다.” |
| detail | string | 선택 | 디버깅용 추가 정보. 내부 로그와 함께 보면 원인 파악에 도움이 됩니다. | “email 형식 오류: foo@bar” |
| traceId | string | 필수 권장 | 요청 단위 추적 ID. 서버 로그와 에러 응답을 연결하는 키입니다. | “9f7c3a0e-0f2b-4f60-8bda-6c93e4f11234” |
| errors | array | 선택 | Validation 등 필드 단위 에러 목록. 각 항목은 field, code, message를 포함합니다. | [{ field: "email", code: "INVALID_EMAIL", ... }] |
| timestamp | string | 선택 | 에러 발생 시각(ISO-8601 등). 운영 이슈 분석에 활용할 수 있습니다. | “2025-12-15T10:23:45.123Z” |
실행 단계
- 에러 바디 공통 포맷 정의
팀 내에서 사용할 에러 바디 구조를 먼저 문서화합니다.status,code,message,detail,traceId,errors등 필드 목록과 타입, 필수 여부를 합의해 둡니다. - 비즈니스 에러 코드 목록 정리
도메인별로 발생 가능한 에러를 정리해 코드 표를 만듭니다. 예:USER_NOT_FOUND,ORDER_CANNOT_CANCEL,VALIDATION_FAILED등. 이때 코드 네이밍 규칙(대문자 스네이크 케이스 등)을 통일하는 것이 좋습니다. - 전역 예외 처리 레이어 구현
Spring의@RestControllerAdvice나 Express의 에러 핸들링 미들웨어처럼, 예외를 한 곳에서 받아 공통 에러 바디로 변환해 주는 레이어를 구현합니다. 비즈니스 예외, Validation 예외, 예상치 못한 예외를 각각 다른 코드로 매핑합니다. - Validation 에러 매핑 로직 추가
Bean Validation, Joi, Zod 등 사용하는 검증 라이브러리에 맞게, 필드별 에러 정보를 파싱하여errors배열에 채워 넣는 로직을 구현합니다. 클라이언트와 필드명, 코드값에 대한 약속을 맞추는 것이 중요합니다. - 로그와 traceId 연동
요청 진입 지점에서 traceId를 생성하고, 로그 MDC(또는 유사 기능)에 넣은 뒤, 에러 응답에도 동일한 값을 내려주도록 구성합니다. 실제 장애 시 traceId를 중심으로 로그와 모니터링 시스템에서 호출 경로를 추적할 수 있습니다. - API 문서에 에러 응답 예시 추가
Swagger/OpenAPI 문서에 공통 에러 응답 스키마와 실제 예시(JSON)를 반드시 포함합니다. 클라이언트 개발자가 에러 구조를 이해하고 대응 로직을 짜기 쉽도록 돕는 과정입니다.
추가로 생각해볼 점
- 다국어(Localized) 메시지 전략
message를 서버에서 다국어 처리할지,code만 내려주고 클라이언트에서 메시지를 번역할지에 대한 전략을 미리 정하는 것이 좋습니다. - 보안 관점의 정보 노출 제한
detail에 스택트레이스나 내부 시스템 이름, SQL 정보 등을 노출하지 않도록 주의해야 합니다. 운영/개발 환경과 상용 환경에서 detail 수준을 다르게 가져가는 방법도 고려할 수 있습니다. - 장기적인 모니터링, 알림 연동
에러 코드별 발생 빈도를 집계하고, 특정 에러 코드가 급증할 경우 알림을 보내는 등 Observability 도구와 연동하면 에러 설계의 가치를 더 크게 활용할 수 있습니다.

0 댓글