REST API 설계 — URI 규칙, 상태 코드, 멱등성 제대로 이해하기
API를 설계할 때, URI를 어떻게 짓고, 어떤 상태 코드를 보내고, 멱등성이 왜 중요한지 — 이 세 가지를 명확히 아는 것만으로도 API 품질이 달라집니다.
REST는 2025년 Postman 조사 기준으로 93%의 API가 채택하는 가장 보편적인 아키텍처 스타일입니다. gRPC(14%)가 내부 통신에서 성장하고 있지만, 공개 API는 여전히 REST가 표준입니다.
REST란 — Roy Fielding의 6가지 제약 조건
REST(Representational State Transfer)는 Roy Fielding이 2000년 박사 논문에서 정의한 아키텍처 스타일입니다. "프로토콜"이 아니라 "제약 조건의 집합"이라는 점이 중요합니다.
- Client-Server: 클라이언트와 서버의 역할 분리
- Stateless: 각 요청은 필요한 모든 정보를 포함 (서버가 세션 상태를 저장하지 않음)
- Cacheable: 응답은 캐시 가능 여부를 명시해야 함
- Layered System: 클라이언트는 중간 계층(프록시, 로드밸런서)의 존재를 알 필요 없음
- Uniform Interface: 리소스 식별, 표현을 통한 조작, 자기 서술적 메시지, HATEOAS
- Code on Demand (선택): 필요시 서버가 실행 가능한 코드를 전달 (예: JavaScript)
실무에서 "RESTful하다"라고 말할 때, 대부분은 1~4번 + Uniform Interface의 일부만 지키는 수준입니다. 완전한 REST(HATEOAS 포함)를 구현하는 API는 드뭅니다.
URI 설계 규칙
기본 원칙
# 좋은 예
GET /users
GET /users/{id}
GET /users/{id}/orders
POST /users
DELETE /users/{id}
# 나쁜 예
GET /getUsers ← 동사 사용
POST /user/create ← 동사 + 단수형
GET /Users/{ID} ← 대문자
GET /user_list ← 언더스코어
- ** 명사 사용 **: 행위는 HTTP 메서드가 표현하므로 URI에 동사를 쓰지 않음
- ** 복수형 **: 컬렉션을 나타내므로
/users가/user보다 자연스러움 - ** 소문자 + 하이픈 **:
/user-profiles(언더스코어나 카멜케이스 지양) - ** 계층 관계 **:
/users/{id}/orders— 사용자에 속한 주문
필터링, 정렬, 페이지네이션
# 쿼리 파라미터로 처리
GET /users?status=active&sort=created_at&page=2&size=20
# URI에 넣지 않음 (안티패턴)
GET /users/active/sort-by-date/page/2 ← 복잡하고 캐시 불리
검색 엔드포인트
# 간단한 검색
GET /users?name=kim
# 복잡한 검색 (여러 조건)
GET /users/search?name=kim&age_min=20&age_max=30
# 매우 복잡한 검색 (본문이 필요한 경우)
POST /users/search
복잡한 검색을 POST로 처리하는 것은 REST 원칙에서 벗어나지만, URL 길이 제한과 실용성 때문에 널리 쓰입니다. Elasticsearch도 이 방식을 사용합니다.
HTTP 메서드와 멱등성
멱등성(Idempotency)이란?
같은 요청을 1번 보내든 100번 보내든 결과가 동일한 성질입니다. 네트워크 오류 시 재시도할 수 있느냐의 기준이 됩니다.
| 메서드 | 용도 | 멱등 | 안전 | 요청 본문 |
|---|---|---|---|---|
| GET | 조회 | O | O | 없음 |
| POST | 생성 | X | X | 있음 |
| PUT | 전체 교체 | O | X | 있음 |
| PATCH | 부분 수정 | △ | X | 있음 |
| DELETE | 삭제 | O | X | 없음 |
- ** 안전(Safe)**: 서버 상태를 변경하지 않음 (GET, HEAD, OPTIONS)
- ** 멱등(Idempotent)**: 같은 요청 반복 시 결과가 동일 (GET, PUT, DELETE)
- **PATCH가 비멱등인 이유 **:
{"op": "increment", "path": "/count", "value": 1}같은 연산은 호출할 때마다 값이 변합니다
PUT vs PATCH
# PUT — 전체 교체 (멱등)
PUT /users/1
{
"name": "김철수",
"email": "kim@example.com",
"age": 30
}
# → age를 빠뜨리면 age가 null이 됨
# PATCH — 부분 수정 (구현에 따라 비멱등)
PATCH /users/1
{
"age": 31
}
# → age만 변경, 나머지 필드 유지
공부하다 보니 PUT과 POST의 차이보다 PUT과 PATCH의 차이에서 더 많이 헷갈렸습니다. 핵심은 PUT은 "이걸로 통째로 바꿔"이고, PATCH는 "이 부분만 고쳐"입니다.
상태 코드 정리
2xx — 성공
| 코드 | 의미 | 사용 상황 |
|---|---|---|
| 200 | OK | 일반적인 성공 (GET, PUT) |
| 201 | Created | 리소스 생성 성공 (POST) |
| 204 | No Content | 성공했지만 응답 본문 없음 (DELETE) |
3xx — 리다이렉션
| 코드 | 의미 | 사용 상황 |
|---|---|---|
| 301 | Moved Permanently | URI가 영구적으로 변경 |
| 302 | Found | 일시적 리다이렉션 |
| 304 | Not Modified | 캐시 유효 (ETag/Last-Modified) |
4xx — 클라이언트 오류
| 코드 | 의미 | 사용 상황 |
|---|---|---|
| 400 | Bad Request | 요청 형식 오류, 유효성 검증 실패 |
| 401 | Unauthorized | 인증 필요 (토큰 없음/만료) |
| 403 | Forbidden | 인증은 됐지만 권한 없음 |
| 404 | Not Found | 리소스 없음 |
| 405 | Method Not Allowed | 지원하지 않는 HTTP 메서드 |
| 409 | Conflict | 리소스 충돌 (중복 생성 등) |
| 429 | Too Many Requests | Rate Limit 초과 |
5xx — 서버 오류
| 코드 | 의미 | 사용 상황 |
|---|---|---|
| 500 | Internal Server Error | 서버 내부 오류 |
| 502 | Bad Gateway | 업스트림 서버 오류 |
| 503 | Service Unavailable | 서버 과부하 또는 점검 중 |
| 504 | Gateway Timeout | 업스트림 서버 응답 시간 초과 |
401과 403의 차이: 401은 "넌 누구야?"(인증 실패), 403은 "넌 알겠는데 권한이 없어"(인가 실패). 헷갈리기 쉬운데 보안 로직에서 이 구분이 중요합니다.
HATEOAS와 Richardson Maturity Model
Richardson Maturity Model
REST API의 성숙도를 4단계로 나눈 모델입니다.
| 레벨 | 설명 | 예시 |
|---|---|---|
| 0 | 단일 URI, 모든 것을 POST로 | POST /api |
| 1 | 리소스별 URI 분리 | POST /users, POST /orders |
| 2 | HTTP 메서드 올바르게 활용 | GET /users, POST /users, DELETE /users/{id} |
| 3 | HATEOAS | 응답에 다음 행동 링크 포함 |
대부분의 실무 API는 Level 2에 머물고, 그것만으로도 충분히 좋은 API입니다.
HATEOAS (Level 3)
{
"id": 1,
"name": "김철수",
"status": "active",
"_links": {
"self": { "href": "/users/1" },
"orders": { "href": "/users/1/orders" },
"deactivate": { "href": "/users/1/deactivate", "method": "POST" }
}
}
클라이언트가 하드코딩된 URL 없이 응답의 링크를 따라가며 탐색할 수 있습니다. 이론적으로 아름답지만, 구현 복잡성 대비 실용적 이점이 적어서 실무에서 완전히 구현하는 경우는 드뭅니다.
API 버저닝
API가 변경될 때 기존 클라이언트를 깨뜨리지 않으려면 버저닝이 필요합니다.
| 방식 | 예시 | 장점 | 단점 |
|---|---|---|---|
| URI | /v1/users | 직관적, 캐시 분리 용이 | URI가 리소스가 아닌 버전을 포함 |
| 헤더 | X-API-Version: 1 | URI가 깔끔 | 테스트하기 불편 |
| Content Negotiation | Accept: application/vnd.api+json;version=1 | HTTP 표준에 가까움 | 복잡하고 인지도 낮음 |
** 실무 권장 **: URI 방식 (/v1/users)이 가장 널리 쓰입니다. curl이나 브라우저에서 바로 테스트할 수 있고, CDN 캐시 키가 자연스럽게 분리됩니다.
정리
- REST는 프로토콜이 아니라 제약 조건의 집합 — 완벽한 REST보다 실용적인 Level 2가 현실적
- URI는 명사 + 복수형 + 소문자, 행위는 HTTP 메서드로 표현
- 멱등성은 재시도 가능 여부를 결정 — GET/PUT/DELETE는 멱등, POST는 비멱등
- 상태 코드는 클라이언트에게 보내는 "결과 요약" — 올바른 코드 사용이 API 품질의 기본
- 버저닝은 URI 방식이 가장 실용적 —
/v1/users