API 설계 원칙 — RESTful 성숙도 모델과 좋은 API의 조건
"RESTful하게 설계했다"고 하는데, REST의 기준이 뭐고 어디까지 지켜야 하는 걸까요?
이게 뭔가요?
API(Application Programming Interface)는 시스템 간 통신 규약이고, REST(Representational State Transfer)는 웹 API를 설계하는 아키텍처 스타일입니다. "좋은 API"란 사용하기 쉽고, 일관되며, 확장 가능한 API를 말합니다.
왜 필요한가요?
- API는 한 번 공개하면 바꾸기가 매우 어려움 (하위 호환성)
- 잘 설계된 API는 문서를 보지 않아도 예측 가능
- 나쁜 API는 클라이언트 개발자의 생산성을 크게 떨어뜨림
Richardson 성숙도 모델
REST API의 성숙도를 4단계로 나눈 모델입니다.
Level 0 — The Swamp of POX
모든 요청이 하나의 URL, 하나의 HTTP 메서드(POST)
POST /api
{
"action": "getUser",
"userId": 1
}
POST /api
{
"action": "createOrder",
"productId": 5,
"quantity": 2
}
HTTP를 단순한 전송 수단으로만 사용합니다. RPC 스타일입니다.
Level 1 — Resources
리소스별로 URL을 분리
POST /users/1 → 사용자 조회
POST /orders → 주문 생성
POST /products/5 → 상품 조회
URL에 리소스 개념이 등장하지만, 여전히 HTTP 메서드를 구분하지 않습니다.
Level 2 — HTTP Verbs
HTTP 메서드를 올바르게 사용
GET /users/1 → 사용자 조회
POST /users → 사용자 생성
PUT /users/1 → 사용자 전체 수정
PATCH /users/1 → 사용자 부분 수정
DELETE /users/1 → 사용자 삭제
대부분의 "RESTful API"가 여기에 해당합니다. 이 정도면 충분히 좋은 API입니다.
Level 3 — HATEOAS
// 응답에 다음 가능한 액션의 링크를 포함
{
"id": 1,
"name": "Alice",
"links": [
{ "rel": "self", "href": "/users/1" },
{ "rel": "orders", "href": "/users/1/orders" },
{ "rel": "update", "href": "/users/1", "method": "PUT" }
]
}
클라이언트가 URL을 하드코딩하지 않고, 서버 응답의 링크를 따라가며 탐색합니다. 이론적으로 가장 이상적이지만, 현실에서는 Level 2로 충분한 경우가 많습니다.
RESTful API 설계 규칙
URL 네이밍
✓ 좋은 예:
GET /users → 사용자 목록
GET /users/1 → 특정 사용자
GET /users/1/orders → 사용자의 주문 목록
GET /users/1/orders/5 → 사용자의 특정 주문
POST /users/1/orders → 사용자의 주문 생성
✗ 나쁜 예:
GET /getUser?id=1 → 동사 사용
GET /user/1 → 단수형
POST /users/1/createOrder → URL에 동사
GET /Users/1 → 대문자
핵심 규칙:
- 명사 복수형 사용 (
/users,/orders) - 소문자 + 하이픈 (
/order-items, not/orderItems) - 계층 관계 는
/로 표현 (/users/1/orders) - 동사는 HTTP 메서드 로 표현 (URL에 동사 넣지 않기)
HTTP 메서드 의미
| 메서드 | 의미 | 멱등성 | 안전성 |
|---|---|---|---|
| GET | 조회 | 멱등 | 안전 |
| POST | 생성 | 비멱등 | 비안전 |
| PUT | 전체 교체 | 멱등 | 비안전 |
| PATCH | 부분 수정 | 비멱등 | 비안전 |
| DELETE | 삭제 | 멱등 | 비안전 |
멱등(Idempotent): 같은 요청을 여러 번 보내도 결과가 동일 안전(Safe): 서버 상태를 변경하지 않음
상태 코드 사용
2xx 성공:
200 OK → 일반적인 성공
201 Created → 리소스 생성 성공 (POST)
204 No Content → 성공, 응답 본문 없음 (DELETE)
4xx 클라이언트 에러:
400 Bad Request → 잘못된 요청 (유효성 검증 실패)
401 Unauthorized → 인증 필요
403 Forbidden → 인증됐지만 권한 없음
404 Not Found → 리소스 없음
409 Conflict → 충돌 (중복 등록 등)
422 Unprocessable → 문법은 맞지만 처리 불가
5xx 서버 에러:
500 Internal Server Error → 서버 내부 오류
503 Service Unavailable → 서비스 일시 중단
에러 응답 형식
// 일관된 에러 응답 구조
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력값이 유효하지 않습니다",
"details": [
{
"field": "email",
"message": "올바른 이메일 형식이 아닙니다"
},
{
"field": "name",
"message": "이름은 2자 이상이어야 합니다"
}
]
}
}
페이지네이션
커서 기반 (추천):
GET /posts?cursor=eyJpZCI6MTAwfQ&limit=20
오프셋 기반 (간단하지만 대용량에서 느림):
GET /posts?page=3&size=20
응답:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_next": true,
"total_count": 1500
}
}
필터링, 정렬, 필드 선택
필터링:
GET /products?category=electronics&min_price=10000
정렬:
GET /products?sort=-price,name (price 내림차순, name 오름차순)
필드 선택:
GET /users/1?fields=id,name,email (필요한 필드만 반환)
REST vs GraphQL
REST:
GET /users/1 → 사용자 전체 정보 반환 (Over-fetching)
GET /users/1/orders → 별도 요청 필요 (Under-fetching)
GraphQL:
POST /graphql
{
query {
user(id: 1) {
name
email
orders(first: 5) {
total
status
}
}
}
}
→ 한 번의 요청으로 필요한 것만 정확히 반환
| 구분 | REST | GraphQL |
|---|---|---|
| 엔드포인트 | 리소스별 여러 개 | 하나 (/graphql) |
| Over/Under-fetching | 발생 가능 | 클라이언트가 제어 |
| 캐싱 | HTTP 캐싱 쉬움 | 캐싱 복잡 |
| 학습 곡선 | 낮음 | 높음 |
| 적합한 경우 | 일반 CRUD API | 복잡한 데이터 관계, 모바일 |
자주 헷갈리는 포인트
-
"PUT과 PATCH의 차이" — PUT은 리소스 전체를 교체하고, PATCH는 변경된 필드만 보냅니다. PUT으로
name만 보내면 나머지 필드가 null이 될 수 있습니다. -
"HATEOAS를 안 하면 RESTful이 아닌가" — 엄밀하게는 맞지만, Level 2(HTTP 메서드 + 리소스)로도 충분히 좋은 API를 만들 수 있습니다.
-
"항상 200을 반환하고 body에 에러 코드를 넣는 방식" — HTTP 상태 코드를 올바르게 사용하는 것이 좋습니다. 상태 코드를 보고 에러 여부를 판단할 수 있어야 합니다.
정리
- REST API는 리소스(명사) + HTTP 메서드(동사) + 상태 코드로 설계
- Richardson 성숙도 Level 2(리소스 + HTTP 메서드)가 현실적인 목표
- URL은 명사 복수형, 소문자, 계층 관계로 구성
- 에러 응답은 일관된 구조로, 상태 코드를 올바르게 사용
- REST가 대부분의 상황에서 적합하고, GraphQL은 복잡한 데이터 관계에서 강점