API 설계 가이드
핵심 원칙
1. 명확하고 예측 가능하게
API를 처음 보는 개발자가 문서 없이도 80% 이해할 수 있어야 합니다.
2. 일관성이 최우선
좋은 규칙을 일관되게 따르는 것이, 완벽한 규칙을 불규칙하게 따르는 것보다 낫습니다.
3. 개발자 경험(DX) 중심
API는 개발자를 위한 제품입니다. 사용하기 쉬워야 합니다.
URL 설계 원칙
✅ 좋은 URL 설계
code
# 명사 사용 (동사 X) GET /users # ✅ GET /getUsers # ❌ # 복수형 사용 GET /users # ✅ GET /user # ❌ # 계층 구조 표현 GET /users/123/orders # ✅ GET /orders?userId=123 # 😐 (대안으로 허용) # 케밥 케이스 사용 GET /user-profiles # ✅ GET /userProfiles # ❌ GET /user_profiles # ❌ # 필터는 쿼리 파라미터로 GET /products?category=electronics&sort=price # ✅ GET /products/electronics/sortByPrice # ❌
❌ 피해야 할 패턴
code
# URL에 동사 포함 POST /users/create # ❌ POST /users # ✅ # URL에 동작 표현 GET /users/search # ❌ GET /users?q=keyword # ✅ # 파일 확장자 포함 GET /users.json # ❌ GET /users # ✅ (Accept 헤더 사용)
HTTP 메서드 사용법
CRUD 매핑
code
# Create - POST
POST /users # 새 사용자 생성
Body: { "name": "홍길동", "email": "hong@example.com" }
# Read - GET
GET /users # 목록 조회
GET /users/123 # 단건 조회
# Update - PUT/PATCH
PUT /users/123 # 전체 수정 (모든 필드 필요)
PATCH /users/123 # 부분 수정 (일부 필드만)
Body: { "name": "김철수" }
# Delete - DELETE
DELETE /users/123 # 삭제
멱등성(Idempotency) 이해하기
code
# 멱등성 O (여러 번 호출해도 같은 결과) GET /users/123 # ✅ 항상 같은 결과 PUT /users/123 # ✅ 같은 데이터로 여러 번 호출해도 같은 상태 DELETE /users/123 # ✅ 두 번째부터는 404지만 상태는 같음 # 멱등성 X (호출할 때마다 결과가 다름) POST /users # ❌ 호출할 때마다 새 사용자 생성 PATCH /orders/123/add-item # ❌ 호출할 때마다 아이템 추가
상태 코드 전략
자주 사용하는 코드만 정확히
code
# 성공 (2xx) 200 OK # GET, PUT, PATCH 성공 201 Created # POST 성공 (Location 헤더와 함께) 204 No Content # DELETE 성공 (응답 body 없음) # 클라이언트 오류 (4xx) 400 Bad Request # 잘못된 요청 (validation 실패) 401 Unauthorized # 인증 필요 403 Forbidden # 권한 없음 404 Not Found # 리소스 없음 409 Conflict # 중복 등 충돌 # 서버 오류 (5xx) 500 Internal Server Error # 서버 오류 503 Service Unavailable # 일시적 서비스 불가
❌ 과도한 상태 코드 사용 피하기
code
# 불필요한 세분화 451 Unavailable For Legal Reasons # ❌ 너무 구체적 403 Forbidden # ✅ 충분함 # 일관성 없는 사용 POST /users → 200 OK (이미 있음) # ❌ POST /users → 409 Conflict # ✅
응답 형식
일관된 JSON 구조
json
// ✅ 성공 응답
{
"data": {
"id": "123",
"name": "홍길동",
"email": "hong@example.com"
}
}
// ✅ 목록 응답
{
"data": [
{ "id": "1", "name": "홍길동" },
{ "id": "2", "name": "김철수" }
],
"pagination": {
"page": 1,
"pageSize": 20,
"totalCount": 100,
"totalPages": 5
}
}
// ✅ 에러 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "이메일 형식이 올바르지 않습니다",
"details": [
{
"field": "email",
"message": "유효한 이메일을 입력하세요"
}
]
}
}
❌ 피해야 할 패턴
json
// ❌ 불일치한 구조
// 성공 시
{ "user": { "id": 1 } }
// 실패 시
{ "error": "..." }
// ✅ 일관된 구조
// 항상 data 또는 error 키 사용
페이지네이션
오프셋 기반 (간단한 경우)
code
GET /users?page=2&pageSize=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"pageSize": 20,
"totalCount": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
커서 기반 (대용량/실시간)
code
GET /posts?cursor=eyJpZCI6MTIzfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"prevCursor": "eyJpZCI6MTAzfQ",
"hasMore": true
}
}
필터링 & 정렬
쿼리 파라미터 설계
code
# 필터링 GET /products?category=electronics&minPrice=10000&maxPrice=50000 # 정렬 GET /products?sort=price # 오름차순 GET /products?sort=-price # 내림차순 (- 접두사) GET /products?sort=price,-rating # 다중 정렬 # 필드 선택 (Sparse Fieldsets) GET /users?fields=id,name,email # 필요한 필드만 # 검색 GET /products?q=노트북 # 통합 검색 GET /products?search=name:노트북 # 필드별 검색
인증 & 보안
Bearer Token 사용
code
# 헤더에 토큰 전달 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ❌ URL에 토큰 포함하지 않기 GET /users?token=xxx # 위험! 로그에 남음
API 키 (공개 API)
code
# 헤더 사용 (권장) X-API-Key: your-api-key-here # 또는 쿼리 파라미터 (읽기 전용 API만) GET /public/data?apiKey=xxx
버전 관리
URL 버전 (가장 명확)
code
GET /v1/users GET /v2/users # 장점: 명확하고 캐싱 쉬움 # 단점: URL이 길어짐
헤더 버전 (RESTful)
code
GET /users Accept: application/vnd.myapp.v2+json # 장점: URL 깔끔 # 단점: 브라우저 테스트 어려움
⚠️ 버전 변경 원칙
code
# 하위 호환성 유지
v1: { "name": "홍길동" }
v2: { "name": "홍길동", "firstName": "길동", "lastName": "홍" } # ✅
v1: { "name": "홍길동" }
v2: { "fullName": "홍길동" } # ❌ name 필드 제거 (breaking change)
관계 표현
중첩 리소스
code
# 사용자의 주문 조회 GET /users/123/orders # 특정 주문 상세 GET /users/123/orders/456
독립 리소스 + 필터
code
# 모든 주문에서 필터링 GET /orders?userId=123 # 언제 사용? # - 리소스가 독립적으로 관리될 때 # - 다양한 필터링이 필요할 때
에러 처리
명확한 에러 메시지
json
// ✅ 좋은 에러 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력값을 확인해주세요",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "올바른 이메일 형식이 아닙니다"
},
{
"field": "password",
"code": "TOO_SHORT",
"message": "비밀번호는 8자 이상이어야 합니다"
}
],
"requestId": "req_123abc"
}
}
// ❌ 나쁜 에러 응답
{
"error": "Error" // 너무 모호함
}
에러 코드 체계
code
# 명명 규칙 VALIDATION_ERROR RESOURCE_NOT_FOUND AUTHENTICATION_REQUIRED INSUFFICIENT_PERMISSIONS RATE_LIMIT_EXCEEDED # ❌ 피할 것 ERR_001 # 의미 불명확 error # 너무 일반적
Rate Limiting
헤더로 정보 제공
code
HTTP/1.1 200 OK X-RateLimit-Limit: 100 # 시간당 한도 X-RateLimit-Remaining: 87 # 남은 요청 수 X-RateLimit-Reset: 1640000000 # 리셋 시각 (Unix timestamp) # 한도 초과 시 HTTP/1.1 429 Too Many Requests Retry-After: 3600 # 초 단위
실전 체크리스트
API 설계 전 확인
- • URL이 명사로만 구성되어 있는가?
- • 복수형을 일관되게 사용하는가?
- • HTTP 메서드를 올바르게 사용하는가?
- • 상태 코드가 의미에 맞는가?
- • 응답 구조가 일관적인가?
- • 에러 메시지가 명확한가?
- • 페이지네이션이 구현되어 있는가?
- • 인증 방식이 안전한가?
- • 버전 관리 전략이 있는가?
- • Rate limiting이 구현되어 있는가?
문서화 필수 항목
- • 모든 엔드포인트 목록
- • 요청/응답 예시
- • 에러 코드 설명
- • 인증 방법
- • Rate limit 정책
나쁜 예 vs 좋은 예
케이스 1: 사용자 관리
code
# ❌ 나쁜 설계 GET /getUser?id=123 POST /createUser POST /updateUser POST /deleteUser # ✅ 좋은 설계 GET /users/123 POST /users PUT /users/123 DELETE /users/123
케이스 2: 검색
code
# ❌ 나쁜 설계 GET /search/users/홍길동 # ✅ 좋은 설계 GET /users?q=홍길동
케이스 3: 복잡한 작업
code
# ❌ 나쁜 설계
POST /orders/123/sendEmail
# ✅ 좋은 설계
POST /orders/123/notifications
Body: { "type": "email" }
# 또는 이벤트 기반
POST /orders/123/confirm # 확인하면 자동으로 이메일 발송
한국 서비스 특화
한글 지원
code
# URL 인코딩
GET /products?q=%ED%99%8D%EA%B8%B8%EB%8F%99
# Accept-Language 헤더
GET /products
Accept-Language: ko-KR
# 응답에 언어 포함
{
"data": {
"name": "상품명",
"nameEn": "Product Name"
}
}
주민등록번호 등 민감정보
code
# ❌ URL에 포함하지 않기
GET /users?residentNumber=123456-1234567
# ✅ POST body에 포함
POST /users/verify
Body: { "residentNumber": "123456-1234567" }
# 응답에서 마스킹
{
"residentNumber": "123456-1******"
}
마무리 원칙
"API는 계약이다. 한 번 배포하면 쉽게 바꿀 수 없다."
- •일관성: 같은 패턴을 반복하라
- •예측 가능성: 놀라움을 주지 마라
- •명확성: 문서 없이도 이해할 수 있게
- •하위 호환성: 기존 클라이언트를 망가뜨리지 마라
- •단순함: 복잡한 것보다 단순한 것이 낫다