- Published on
REST API 설계 베스트 프랙티스 2025: 네이밍, 버저닝, 에러 처리, 페이지네이션, 보안
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 서론
- 1. REST 원칙 (Principles)
- 2. 리소스 네이밍 (Resource Naming)
- 3. HTTP 메서드 올바른 사용
- 4. 상태 코드 (Status Codes)
- 5. 에러 응답 설계
- 6. 페이지네이션 (Pagination)
- 7. 필터링, 정렬, 필드 선택
- 8. 버저닝 (Versioning)
- 9. 인증과 보안 (Authentication and Security)
- 10. 레이트 리미팅 (Rate Limiting)
- 11. HATEOAS
- 12. 벌크 작업과 비동기 처리
- 13. OpenAPI 3.1 스펙
- 14. API 설계 안티패턴
- 15. GraphQL vs REST vs gRPC 비교
- 16. 퀴즈
- 참고 자료
서론
REST API는 현대 웹 서비스의 근간입니다. 모바일 앱, SPA, 마이크로서비스, IoT 기기 등 거의 모든 소프트웨어가 REST API를 통해 통신합니다. 하지만 "RESTful"이라고 주장하면서도 실제로는 REST 원칙을 위반하는 API가 놀라울 정도로 많습니다.
잘 설계된 API는 개발자 경험(DX)을 향상시키고, 유지보수를 쉽게 하며, 확장성과 보안을 보장합니다. 반대로 잘못 설계된 API는 팀 간 소통 비용을 증가시키고, 보안 취약점을 만들며, 기술 부채를 쌓습니다.
이 가이드에서는 리소스 네이밍부터 버저닝, 에러 처리, 인증, OpenAPI까지 REST API 설계의 모든 베스트 프랙티스를 다룹니다.
1. REST 원칙 (Principles)
1.1 REST의 6가지 제약 조건
1. Client-Server (클라이언트-서버 분리)
- UI와 데이터 저장 관심사 분리
- 독립적 진화 가능
2. Stateless (무상태)
- 각 요청은 완전한 정보를 포함
- 서버는 클라이언트 상태를 저장하지 않음
- 확장성 향상
3. Cacheable (캐시 가능)
- 응답에 캐시 가능 여부 명시
- Cache-Control, ETag, Last-Modified 헤더 활용
4. Uniform Interface (통일된 인터페이스)
- 리소스 식별 (URI)
- 표현을 통한 리소스 조작
- 자기 서술적 메시지
- HATEOAS
5. Layered System (계층 구조)
- 클라이언트는 직접 서버와 통신하는지 중간 계층과 통신하는지 알 수 없음
- 로드 밸런서, 캐시, 게이트웨이 등 추가 가능
6. Code-on-Demand (선택 사항)
- 서버가 실행 가능한 코드를 클라이언트에 전송 가능
- 유일한 선택적 제약
1.2 REST의 성숙도 모델 (Richardson Maturity Model)
Level 3: Hypermedia Controls (HATEOAS)
└── 응답에 관련 리소스 링크 포함
Level 2: HTTP Methods
└── GET, POST, PUT, DELETE 올바른 사용
Level 1: Resources
└── 개별 리소스에 URI 부여
Level 0: The Swamp of POX
└── 단일 엔드포인트, 모든 작업을 POST로
대부분의 API는 Level 2를 목표로 합니다.
Level 3 (HATEOAS)는 이상적이지만 실무에서는 선택적입니다.
2. 리소스 네이밍 (Resource Naming)
2.1 기본 규칙
Good:
GET /users # 사용자 목록
GET /users/123 # 특정 사용자
POST /users # 사용자 생성
PUT /users/123 # 사용자 전체 수정
PATCH /users/123 # 사용자 부분 수정
DELETE /users/123 # 사용자 삭제
Bad:
GET /getUsers # 동사 사용 금지
GET /user/123 # 복수형 사용
POST /createUser # 동사 사용 금지
POST /user/123/delete # HTTP 메서드 활용
2.2 계층 관계 표현
# 사용자의 주문 목록
GET /users/123/orders
# 사용자의 특정 주문
GET /users/123/orders/456
# 주문의 아이템 목록
GET /users/123/orders/456/items
# 주의: 3단계 이상 중첩은 피하기
# Bad:
GET /users/123/orders/456/items/789/reviews
# Good (대안):
GET /items/789/reviews
GET /reviews?item_id=789
2.3 네이밍 컨벤션
# 소문자 + 하이픈 (kebab-case) 사용
Good: /user-profiles
Bad: /userProfiles, /user_profiles, /UserProfiles
# 파일 확장자 사용 금지
Good: Accept: application/json
Bad: /users/123.json
# 후행 슬래시 사용 금지
Good: /users
Bad: /users/
# 필터링은 쿼리 파라미터로
GET /users?status=active&role=admin
GET /products?category=electronics&min_price=100&max_price=500
# 정렬
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at,+name # - 내림차순, + 오름차순
# 필드 선택 (Sparse Fieldsets)
GET /users/123?fields=name,email,avatar
2.4 리소스가 아닌 것들 처리
# 동작(Action)이 필요한 경우: 하위 리소스로 표현
POST /users/123/activate # 사용자 활성화
POST /users/123/deactivate # 사용자 비활성화
POST /orders/456/cancel # 주문 취소
POST /payments/789/refund # 결제 환불
# 검색 (리소스가 아닌 동사적 행위)
GET /search?q=keyword&type=users
# 배치 작업
POST /users/batch
Body: { "ids": [1, 2, 3], "action": "deactivate" }
# 집계
GET /orders/statistics
GET /dashboard/metrics
3. HTTP 메서드 올바른 사용
3.1 메서드별 특성
| 메서드 | 목적 | 멱등성 | 안전성 | 요청 Body | 응답 Body |
|---|---|---|---|---|---|
| GET | 조회 | Yes | Yes | No | Yes |
| POST | 생성 | No | No | Yes | Yes |
| PUT | 전체 교체 | Yes | No | Yes | 선택 |
| PATCH | 부분 수정 | No* | No | Yes | Yes |
| DELETE | 삭제 | Yes | No | 선택 | 선택 |
| HEAD | 헤더만 조회 | Yes | Yes | No | No |
| OPTIONS | 지원 메서드 확인 | Yes | Yes | No | Yes |
*PATCH는 구현에 따라 멱등일 수 있음
3.2 멱등성 (Idempotency) 상세
# PUT - 멱등 (같은 요청을 여러 번 보내도 결과 동일)
PUT /users/123
Body: { "name": "Alice", "email": "alice@example.com" }
# 1번 보내든 10번 보내든 결과는 동일
# DELETE - 멱등
DELETE /users/123
# 첫 번째: 200 OK (삭제됨)
# 두 번째: 404 Not Found (이미 삭제됨) - 부작용 없음
# POST - 비멱등 (같은 요청을 보낼 때마다 새 리소스 생성)
POST /orders
Body: { "product_id": 1, "quantity": 2 }
# 2번 보내면 주문이 2개 생성됨!
멱등성 키로 POST를 안전하게 만들기:
POST /payments
Headers:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: { "amount": 10000, "currency": "KRW" }
# 같은 Idempotency-Key로 재전송하면 중복 결제 방지
# 서버는 키를 저장하고 동일 키 요청시 이전 응답 반환
3.3 PUT vs PATCH
# 원본 데이터
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"role": "user",
"avatar": "default.png"
}
# PUT: 전체 교체 (누락된 필드는 초기화됨)
PUT /users/123
Body: { "name": "Alice Updated", "email": "alice@new.com" }
# 결과: role과 avatar가 null/기본값으로!
# PATCH: 부분 수정 (보낸 필드만 변경)
PATCH /users/123
Body: { "name": "Alice Updated" }
# 결과: name만 변경, 나머지 필드 유지
4. 상태 코드 (Status Codes)
4.1 주요 상태 코드
2xx 성공:
200 OK - 일반 성공 (GET, PUT, PATCH, DELETE)
201 Created - 리소스 생성 (POST) + Location 헤더
202 Accepted - 비동기 처리 수락
204 No Content - 성공, 응답 본문 없음 (DELETE)
3xx 리디렉션:
301 Moved Permanently - 영구 이동
302 Found - 임시 이동
304 Not Modified - 캐시된 버전 사용 가능
4xx 클라이언트 에러:
400 Bad Request - 잘못된 요청 (유효성 검사 실패)
401 Unauthorized - 인증 필요 (로그인 안 됨)
403 Forbidden - 인가 실패 (권한 없음)
404 Not Found - 리소스 없음
405 Method Not Allowed - 지원하지 않는 HTTP 메서드
409 Conflict - 리소스 충돌 (중복 생성 등)
422 Unprocessable Entity - 문법은 맞지만 의미가 잘못됨
429 Too Many Requests - 레이트 리밋 초과
5xx 서버 에러:
500 Internal Server Error - 서버 내부 오류
502 Bad Gateway - 업스트림 서버 오류
503 Service Unavailable - 서비스 일시 중단
504 Gateway Timeout - 업스트림 서버 타임아웃
4.2 상태 코드 선택 가이드
POST로 리소스 생성 성공 → 201 Created
Response Headers:
Location: /users/124
Response Body:
{ "id": 124, "name": "Bob", ... }
DELETE 성공:
옵션 1: 204 No Content (본문 없음)
옵션 2: 200 OK + 삭제된 리소스 정보
인증 vs 인가:
로그인하지 않음 → 401 Unauthorized
로그인했지만 권한 없음 → 403 Forbidden
유효성 검사 실패:
JSON 파싱 실패 → 400 Bad Request
필드 값이 잘못됨 → 422 Unprocessable Entity
리소스 충돌:
이메일 중복 → 409 Conflict
낙관적 락 실패 → 409 Conflict
5. 에러 응답 설계
5.1 RFC 7807 Problem Details
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/users/123",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 0 and 150"
}
],
"timestamp": "2025-03-25T10:30:00Z",
"trace_id": "abc-123-def-456"
}
5.2 에러 코드 체계
에러 코드 네이밍 컨벤션:
AUTH_001 - 인증 토큰 만료
AUTH_002 - 잘못된 인증 정보
AUTH_003 - 계정 잠김
USER_001 - 사용자를 찾을 수 없음
USER_002 - 이메일 중복
USER_003 - 비밀번호 정책 불일치
ORDER_001 - 재고 부족
ORDER_002 - 결제 실패
ORDER_003 - 이미 취소된 주문
RATE_001 - API 호출 한도 초과
RATE_002 - 일일 한도 초과
5.3 에러 응답 구현 예시
from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
class APIError(Exception):
def __init__(self, status, error_type, title, detail, errors=None):
self.status = status
self.error_type = error_type
self.title = title
self.detail = detail
self.errors = errors or []
@app.errorhandler(APIError)
def handle_api_error(error):
response = {
"type": f"https://api.example.com/errors/{error.error_type}",
"title": error.title,
"status": error.status,
"detail": error.detail,
}
if error.errors:
response["errors"] = error.errors
return jsonify(response), error.status
@app.errorhandler(404)
def not_found(e):
return jsonify({
"type": "https://api.example.com/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "The requested resource does not exist."
}), 404
# 사용 예시
@app.route('/users', methods=['POST'])
def create_user():
errors = []
if not request.json.get('email'):
errors.append({
"field": "email",
"code": "REQUIRED",
"message": "Email is required"
})
if errors:
raise APIError(
status=422,
error_type="validation-error",
title="Validation Error",
detail="One or more fields failed validation.",
errors=errors
)
6. 페이지네이션 (Pagination)
6.1 Offset 기반
GET /users?page=3&per_page=20
Response:
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total_items": 1543,
"total_pages": 78,
"has_next": true,
"has_prev": true
}
}
장점: 구현 간단, 페이지 번호로 직접 이동 가능
단점: 대용량에서 느림 (OFFSET 10000), 데이터 변경시 중복/누락 발생
Offset의 문제:
-- page=500, per_page=20일 때
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 9980;
-- DB는 9980개를 읽고 버린 후 20개를 반환 -> 매우 비효율적!
6.2 Cursor 기반 (권장)
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ==",
"prev_cursor": "eyJpZCI6MTI0fQ==",
"has_next": true,
"has_prev": true,
"limit": 20
}
}
Cursor 구현:
import base64
import json
def encode_cursor(last_item):
"""마지막 항목의 정렬 키를 커서로 인코딩"""
cursor_data = {"id": last_item["id"], "created_at": last_item["created_at"]}
return base64.urlsafe_b64encode(
json.dumps(cursor_data).encode()
).decode()
def decode_cursor(cursor_str):
"""커서를 디코딩하여 정렬 키 추출"""
return json.loads(
base64.urlsafe_b64decode(cursor_str.encode()).decode()
)
def get_users(cursor=None, limit=20):
query = "SELECT * FROM users"
if cursor:
decoded = decode_cursor(cursor)
# WHERE 절로 커서 이후 데이터만 조회
query += f" WHERE id > {decoded['id']}"
query += f" ORDER BY id ASC LIMIT {limit + 1}"
results = db.execute(query)
has_next = len(results) > limit
items = results[:limit]
return {
"data": items,
"pagination": {
"next_cursor": encode_cursor(items[-1]) if has_next else None,
"has_next": has_next,
"limit": limit
}
}
6.3 Keyset 기반
-- Cursor의 핵심은 Keyset Pagination
-- 복합 정렬 키 사용 예시
-- 첫 페이지
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 다음 페이지 (마지막 항목: created_at='2025-03-20', id=456)
SELECT * FROM orders
WHERE (created_at, id) < ('2025-03-20', 456)
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 인덱스가 있으면 매우 빠름!
CREATE INDEX idx_orders_cursor ON orders(created_at DESC, id DESC);
6.4 페이지네이션 비교
| 방식 | 성능 | 일관성 | 특정 페이지 이동 | 구현 복잡도 |
|---|---|---|---|---|
| Offset | 대용량에서 느림 | 데이터 변경시 문제 | 가능 | 낮음 |
| Cursor | 일정한 성능 | 일관성 유지 | 불가 | 중간 |
| Keyset | 매우 빠름 | 일관성 유지 | 불가 | 중간 |
7. 필터링, 정렬, 필드 선택
7.1 필터링
# 기본 필터링
GET /products?category=electronics&status=available
# 범위 필터
GET /products?min_price=100&max_price=500
GET /orders?created_after=2025-01-01&created_before=2025-03-25
# 다중 값 필터
GET /products?tags=phone,tablet,laptop
# 검색
GET /products?q=wireless+keyboard
# 복합 필터 (고급)
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500
7.2 정렬
# 단일 필드 정렬
GET /users?sort=name # 오름차순
GET /users?sort=-name # 내림차순
# 다중 필드 정렬
GET /users?sort=-created_at,name
# JSON:API 스타일
GET /articles?sort=-published_at,title
7.3 필드 선택 (Sparse Fieldsets)
# 필요한 필드만 요청 (대역폭 절약)
GET /users/123?fields=id,name,email
# 관계 리소스 포함 (임베딩)
GET /users/123?include=orders,profile
GET /articles/456?include=author,comments&fields[articles]=title,body&fields[author]=name
# GraphQL 스타일 vs REST
# REST: /users/123?fields=name,email&include=orders
# GraphQL: query { user(id: 123) { name email orders { id total } } }
8. 버저닝 (Versioning)
8.1 URL 경로 버저닝 (가장 일반적)
GET /v1/users/123
GET /v2/users/123
장점:
- 명확하고 직관적
- 브라우저에서 테스트 쉬움
- 라우팅 분리 용이
단점:
- URL이 리소스 위치를 나타내야 한다는 REST 원칙 위반
- 버전 변경시 모든 URL 변경 필요
8.2 헤더 버저닝
# Accept 헤더 (Content Negotiation)
GET /users/123
Accept: application/vnd.myapi.v2+json
# 커스텀 헤더
GET /users/123
X-API-Version: 2
장점: URL 깔끔, REST 원칙에 부합
단점: 테스트 불편, 디버깅 어려움
8.3 버전 관리 전략
1. 호환성 유지 원칙:
- 필드 추가는 비파괴적 (기존 클라이언트 영향 없음)
- 필드 제거/이름 변경은 파괴적 → 새 버전 필요
- 필드 타입 변경은 파괴적 → 새 버전 필요
2. Deprecation 프로세스:
Phase 1: 새 버전 출시 + 기존 버전에 Sunset 헤더 추가
Phase 2: 기존 버전에 경고 응답 추가
Phase 3: 기존 버전 종료 (최소 6개월 유예)
HTTP Headers:
Sunset: Sat, 01 Jan 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/v2>; rel="successor-version"
3. API 변경 로그:
- 모든 변경사항 문서화
- 마이그레이션 가이드 제공
- 클라이언트에 사전 알림
9. 인증과 보안 (Authentication and Security)
9.1 인증 방식 비교
| 방식 | 사용 사례 | 장점 | 단점 |
|---|---|---|---|
| API Key | 서버 간 통신, 단순 API | 구현 간단 | 세분화된 권한 어려움 |
| OAuth 2.0 | 사용자 대신 작업 | 표준화, 범위 제어 | 구현 복잡 |
| JWT | 무상태 인증 | 서버 확장 용이 | 토큰 폐기 어려움 |
| Session | 전통적 웹앱 | 서버 제어 쉬움 | 확장성 제한 |
9.2 JWT (JSON Web Token)
JWT 구조:
Header.Payload.Signature
Header:
{
"alg": "RS256",
"typ": "JWT"
}
Payload:
{
"sub": "user-123",
"email": "alice@example.com",
"roles": ["admin", "user"],
"iat": 1711353000,
"exp": 1711356600
}
Signature:
RS256(base64(header) + "." + base64(payload), privateKey)
JWT 사용 패턴:
# Access Token + Refresh Token
Access Token: 15분 유효
Refresh Token: 7일 유효
1. 로그인 → Access + Refresh 토큰 발급
2. API 호출 → Authorization: Bearer {access_token}
3. Access Token 만료 → Refresh Token으로 갱신
4. Refresh Token 만료 → 재로그인
# 헤더 예시
GET /users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
9.3 OAuth 2.0 플로우
Authorization Code Flow (웹 앱):
1. 사용자 → 인가 서버로 리디렉트
2. 사용자 로그인 + 권한 동의
3. 인가 코드를 콜백 URL로 반환
4. 인가 코드로 Access Token 교환
5. Access Token으로 API 호출
Client Credentials Flow (서버 간):
1. 클라이언트 ID + Secret으로 직접 토큰 요청
2. Access Token 발급
3. API 호출
9.4 API 보안 체크리스트
1. HTTPS 필수 (HTTP 절대 금지)
2. 인증 토큰은 헤더로만 전송 (URL 파라미터 금지)
3. 입력 유효성 검사 (SQL Injection, XSS 방지)
4. CORS 설정 (허용 도메인만)
5. Rate Limiting 적용
6. 민감 데이터 로깅 금지
7. 에러 메시지에 내부 정보 노출 금지
8. API Key는 환경 변수로 관리
9. 정기적인 보안 감사
10. 의존성 취약점 스캔
10. 레이트 리미팅 (Rate Limiting)
10.1 응답 헤더
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000 # 시간당 최대 요청 수
X-RateLimit-Remaining: 742 # 남은 요청 수
X-RateLimit-Reset: 1711360000 # 리셋 시간 (Unix timestamp)
# 한도 초과시
HTTP/1.1 429 Too Many Requests
Retry-After: 60 # 60초 후 재시도
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711360000
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 1000 requests per hour.",
"retry_after": 60
}
10.2 레이트 리미팅 전략
1. 사용자별 제한:
- 인증된 사용자: 1000 req/hour
- 미인증 사용자: 100 req/hour
2. 엔드포인트별 제한:
- GET /users: 100 req/min
- POST /users: 10 req/min
- POST /payments: 5 req/min
3. 티어별 제한:
- Free: 100 req/day
- Basic: 10,000 req/day
- Pro: 100,000 req/day
- Enterprise: Custom
4. 구현 (Redis):
key = "rate:user:123:2025-03-25-10" # 사용자 + 시간 윈도우
INCR key
EXPIRE key 3600
11. HATEOAS
11.1 HATEOAS란?
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"status": "active",
"_links": {
"self": {
"href": "/users/123"
},
"orders": {
"href": "/users/123/orders"
},
"deactivate": {
"href": "/users/123/deactivate",
"method": "POST"
},
"update": {
"href": "/users/123",
"method": "PUT"
}
}
}
11.2 HATEOAS 장단점
장점:
- API 탐색 가능 (self-documenting)
- 클라이언트가 URL 하드코딩 불필요
- 서버가 URL 구조 변경 가능
- 가능한 동작을 응답에서 발견
단점:
- 응답 크기 증가
- 구현 복잡도 증가
- 대부분의 클라이언트가 활용하지 않음
- 실무에서 필수가 아님
결론: 공개 API에서는 권장, 내부 API에서는 선택적
12. 벌크 작업과 비동기 처리
12.1 배치 엔드포인트
// POST /users/batch
{
"operations": [
{ "method": "POST", "body": { "name": "Alice", "email": "a@ex.com" } },
{ "method": "POST", "body": { "name": "Bob", "email": "b@ex.com" } },
{ "method": "POST", "body": { "name": "Carol", "email": "c@ex.com" } }
]
}
// Response: 207 Multi-Status
{
"results": [
{ "status": 201, "data": { "id": 1, "name": "Alice" } },
{ "status": 201, "data": { "id": 2, "name": "Bob" } },
{ "status": 409, "error": { "code": "USER_002", "message": "Email already exists" } }
]
}
12.2 비동기 처리 패턴
# 장시간 작업 (보고서 생성, 대량 데이터 처리)
POST /reports
Body: { "type": "annual", "year": 2025 }
Response: 202 Accepted
{
"task_id": "task-abc-123",
"status": "processing",
"estimated_time": 120,
"_links": {
"status": { "href": "/tasks/task-abc-123" },
"cancel": { "href": "/tasks/task-abc-123/cancel", "method": "POST" }
}
}
# 상태 확인
GET /tasks/task-abc-123
{
"task_id": "task-abc-123",
"status": "completed",
"result": {
"download_url": "/reports/2025-annual.pdf"
},
"completed_at": "2025-03-25T10:35:00Z"
}
13. OpenAPI 3.1 스펙
13.1 OpenAPI 기본 구조
openapi: "3.1.0"
info:
title: User Management API
description: API for managing users
version: "1.0.0"
contact:
email: api@example.com
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging-api.example.com/v1
description: Staging
paths:
/users:
get:
summary: List users
operationId: listUsers
tags:
- Users
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
- name: cursor
in: query
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
pagination:
$ref: "#/components/schemas/Pagination"
post:
summary: Create user
operationId: createUser
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"422":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ProblemDetails"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
required:
- id
- name
- email
CreateUserRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
email:
type: string
format: email
required:
- name
- email
13.2 OpenAPI 코드 생성
# OpenAPI에서 코드 자동 생성
1. 클라이언트 SDK 생성:
npx openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./generated-client
2. 서버 스텁 생성:
npx openapi-generator-cli generate \
-i openapi.yaml \
-g python-flask \
-o ./generated-server
3. 모의 서버:
npx prism mock openapi.yaml
4. API 문서:
npx redocly build-docs openapi.yaml
14. API 설계 안티패턴
14.1 10가지 흔한 실수
1. 동사를 URL에 사용
Bad: POST /createUser
Good: POST /users
2. 일관성 없는 네이밍
Bad: /users, /getOrders, /product-list
Good: /users, /orders, /products
3. 단수형 리소스명
Bad: /user/123
Good: /users/123
4. HTTP 메서드 무시
Bad: POST /users/123/delete
Good: DELETE /users/123
5. 항상 200 반환
Bad: 200 OK { "error": true, "message": "Not found" }
Good: 404 Not Found { "title": "Not Found", ... }
6. 중첩 과다
Bad: /countries/kr/cities/seoul/districts/gangnam/restaurants
Good: /restaurants?city=seoul&district=gangnam
7. 버전 없는 API
Bad: /users (언제든 바뀔 수 있음)
Good: /v1/users
8. 에러 응답 표준 없음
Bad: { "error": "something went wrong" }
Good: RFC 7807 Problem Details 형식
9. 페이지네이션 없음
Bad: GET /logs (100만 건 전체 반환)
Good: GET /logs?limit=50&cursor=abc
10. 보안 헤더 누락
Bad: HTTP 허용, CORS 전체 허용
Good: HTTPS 필수, 최소 CORS, 적절한 헤더
15. GraphQL vs REST vs gRPC 비교
┌─────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 특성 │ REST │ GraphQL │ gRPC │
├─────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 프로토콜 │ HTTP/1.1, 2 │ HTTP/1.1, 2 │ HTTP/2 │
│ 데이터 형식 │ JSON/XML │ JSON │ Protocol Buffers │
│ 스키마 │ OpenAPI (선택) │ SDL (필수) │ .proto (필수) │
│ 오버/언더 │ 오버페칭 가능 │ 클라이언트 결정 │ 정확한 스키마 │
│ 페칭 │ │ │ │
│ 실시간 │ WebSocket/SSE │ Subscriptions │ Bidirectional │
│ │ │ │ Streaming │
│ 사용 사례 │ 공개 API, │ 복잡한 데이터 │ 마이크로서비스 │
│ │ 단순 CRUD │ 관계, 모바일 │ 내부 통신 │
│ 학습 곡선 │ 낮음 │ 중간 │ 높음 │
│ 캐싱 │ HTTP 캐싱 용이 │ 어려움 │ 없음 │
│ 에러 처리 │ HTTP 상태 코드 │ errors 배열 │ Status codes │
└─────────────┴──────────────────┴──────────────────┴──────────────────┘
선택 가이드:
REST 선택:
- 공개 API (외부 개발자 대상)
- 간단한 CRUD 작업
- HTTP 캐싱 필요
- 브라우저에서 직접 호출
GraphQL 선택:
- 다양한 클라이언트 (웹, 모바일, IoT)
- 복잡한 데이터 관계
- 오버페칭/언더페칭 문제 해결 필요
- 빠른 프론트엔드 개발
gRPC 선택:
- 마이크로서비스 간 내부 통신
- 낮은 지연 시간 요구
- 양방향 스트리밍 필요
- 강타입 계약 필요
16. 퀴즈
Q1. REST 원칙
REST의 6가지 제약 조건 중 "Stateless"가 확장성에 기여하는 이유를 설명하세요.
답변: Stateless 제약은 서버가 클라이언트의 상태를 저장하지 않으므로, 각 요청이 처리에 필요한 모든 정보를 포함합니다. 이 덕분에:
- 로드 밸런싱이 단순: 어떤 서버든 동일하게 요청 처리 가능 (세션 고정 불필요)
- 수평 확장 용이: 서버 추가/제거가 자유로움
- 장애 복구 쉬움: 서버 장애시 다른 서버가 즉시 대체 가능
- 캐싱 효율 향상: 요청이 독립적이므로 캐싱 키 생성이 명확
Q2. 페이지네이션
Offset 페이지네이션 대신 Cursor 페이지네이션을 선택해야 하는 상황 2가지를 설명하세요.
답변:
-
대용량 데이터셋: 수백만 건의 데이터에서 Offset은
OFFSET 100000처럼 많은 행을 건너뛰어야 하므로 매우 느립니다. Cursor는 인덱스를 활용하여 WHERE 절로 직접 이동하므로 데이터 양에 관계없이 일정한 성능을 보입니다. -
실시간 데이터: 데이터가 빈번하게 추가/삭제되는 경우, Offset은 같은 페이지를 다시 요청할 때 중복이나 누락이 발생합니다. Cursor는 마지막으로 본 항목의 고유 식별자를 기준으로 하므로 일관성을 유지합니다.
Q3. 에러 처리
401 Unauthorized와 403 Forbidden의 차이를 구체적 예시로 설명하세요.
답변:
-
401 Unauthorized (인증 실패): 클라이언트가 자신이 누구인지 증명하지 못한 상태입니다. 예를 들어, Authorization 헤더 없이 API를 호출하거나, 만료된 JWT 토큰을 사용한 경우입니다. "당신이 누구인지 모르겠습니다. 로그인하세요."
-
403 Forbidden (인가 실패): 클라이언트가 인증은 되었지만, 해당 리소스에 접근할 권한이 없는 상태입니다. 예를 들어, 일반 사용자가 관리자 전용 API를 호출한 경우입니다. "당신이 누구인지 알지만, 이 리소스에 접근할 권한이 없습니다."
Q4. 버저닝
URL 경로 버저닝(/v1/users)과 Accept 헤더 버저닝의 장단점을 비교하세요.
답변: URL 경로 버저닝 (/v1/users):
- 장점: 직관적, 브라우저에서 테스트 쉬움, 라우팅 분리 용이, 캐싱 쉬움
- 단점: REST 원칙 위반 (URL은 리소스 위치), 모든 URL 변경 필요
Accept 헤더 버저닝 (Accept: application/vnd.myapi.v2+json):
- 장점: URL 깔끔, REST 원칙 부합, 하나의 URL로 여러 버전
- 단점: 테스트 불편 (curl/Postman 필요), 디버깅 어려움, 캐싱 복잡
실무에서는 URL 경로 버저닝이 압도적으로 많이 사용됩니다 (GitHub, Stripe, Google 등).
Q5. 보안
JWT의 장단점과 토큰 탈취시 대응 방법을 설명하세요.
답변: 장점: 서버 세션 저장 불필요 (Stateless), 수평 확장 용이, 마이크로서비스 간 전파 용이, 클레임에 사용자 정보 포함
단점: 토큰 폐기 어려움 (만료 전까지 유효), 토큰 크기가 세션 ID보다 큼, Payload 암호화 안 됨 (인코딩만)
토큰 탈취 대응:
- Access Token 유효 시간 짧게 설정 (15분)
- Refresh Token 회전 (사용시 새 토큰 발급, 기존 무효화)
- 토큰 블랙리스트 (Redis에 폐기된 토큰 저장)
- IP/User-Agent 바인딩
- 의심 활동 감지시 모든 Refresh Token 무효화
참고 자료
- RFC 7231 - HTTP/1.1 Semantics and Content - https://tools.ietf.org/html/rfc7231
- RFC 7807 - Problem Details for HTTP APIs - https://tools.ietf.org/html/rfc7807
- OpenAPI Specification 3.1 - https://spec.openapis.org/oas/v3.1.0
- JSON:API Specification - https://jsonapi.org/
- Google API Design Guide - https://cloud.google.com/apis/design
- Microsoft REST API Guidelines - https://github.com/microsoft/api-guidelines
- Stripe API Reference - https://stripe.com/docs/api
- GitHub REST API - https://docs.github.com/en/rest
- Zalando RESTful API Guidelines - https://opensource.zalando.com/restful-api-guidelines/
- REST API Design Rulebook - Mark Masse (O'Reilly)
- OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
- JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
- Roy Fielding's Dissertation - https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- Richardson Maturity Model - https://martinfowler.com/articles/richardsonMaturityModel.html