REST API 설계 모범 사례: 개발자가 사랑하는 API 만들기
· 12분 읽기
📑 목차
잘 설계된 API는 사용하기 즐겁습니다. 잘못 설계된 API는 좌절감, 버그, 그리고 팀의 리소스를 소진시키는 지원 티켓을 만들어냅니다.
API가 현대 소프트웨어 아키텍처의 중추가 되면서 — 마이크로서비스, 모바일 앱, 서드파티 통합, AI 에이전트를 연결하면서 — 설계를 올바르게 하는 것이 그 어느 때보다 중요해졌습니다. 성공적인 API와 개발자들이 포기하는 API의 차이는 단순히 기능에 관한 것이 아닙니다. 예측 가능성, 일관성, 그리고 개발자 경험에 관한 것입니다.
이 종합 가이드는 훌륭한 API를 평범한 API와 구분하는 실무 사례들을 다루며, 오늘 바로 구현할 수 있는 실제 사례와 실행 가능한 조언을 제공합니다.
REST API 기초
REST(Representational State Transfer)는 엄격한 프로토콜이 아닌 아키텍처 스타일입니다. 핵심 원칙을 이해하면 API 개발 프로세스 전반에 걸쳐 더 나은 설계 결정을 내릴 수 있습니다.
REST 아키텍처의 6가지 기본 제약 조건은 다음과 같습니다:
- 클라이언트-서버 분리: 클라이언트와 서버는 독립적으로 작동하여 각각 별도로 발전할 수 있습니다
- 무상태 통신: 각 요청에는 필요한 모든 정보가 포함되며, 서버는 요청 간에 클라이언트 컨텍스트를 저장하지 않습니다
- 캐시 가능한 응답: 응답은 성능 향상을 위해 캐시할 수 있는지 여부를 명시적으로 나타냅니다
- 균일한 인터페이스: 리소스는 요청에서 식별되며, 클라이언트는 표현을 통해 리소스를 조작합니다
- 계층화된 시스템: 클라이언트는 최종 서버에 직접 연결되어 있는지 중개자에 연결되어 있는지 알 수 없습니다
- 주문형 코드(선택 사항): 서버는 실행 가능한 코드를 전송하여 클라이언트 기능을 확장할 수 있습니다
실제로 REST API는 HTTP 메서드를 의미론적으로 사용합니다: GET은 데이터를 검색하고, POST는 리소스를 생성하며, PUT은 전체 리소스를 대체하고, PATCH는 부분적으로 업데이트하며, DELETE는 리소스를 제거합니다. URL은 동사가 아닌 명사로 리소스를 나타냅니다.
프로 팁: 무상태성은 종종 유지하기 가장 어려운 원칙입니다. 서버에 세션 데이터를 저장하지 마세요. 대신 필요한 모든 인증 및 권한 정보를 포함하는 토큰(예: JWT)을 사용하세요.
URL 설계: 리소스와 네이밍
URL 구조는 개발자가 API를 탐색할 때 가장 먼저 접하는 것입니다. 직관적이고 예측 가능한 URL은 인지 부하를 줄이고 API를 배우고 기억하기 쉽게 만듭니다.
리소스 지향 설계
API를 동작(동사)이 아닌 리소스(명사)를 노출하는 것으로 생각하세요. HTTP 메서드가 동작을 나타내므로 URL은 작업 대상만 식별해야 합니다.
| 좋음 ✅ | 나쁨 ❌ | 이유 |
|---|---|---|
GET /users |
GET /getUsers |
HTTP 메서드가 이미 "get"을 의미함 |
GET /users/123 |
GET /user?id=123 |
리소스 식별자는 경로에 속함 |
POST /users |
POST /createUser |
HTTP 메서드가 "create"를 의미함 |
DELETE /users/123 |
POST /deleteUser/123 |
적절한 HTTP 메서드 사용 |
GET /users/123/orders |
GET /getUserOrders?userId=123 |
계층적 관계가 더 명확함 |
네이밍 규칙
네이밍의 일관성은 혼란을 방지하고 오류를 줄입니다. 다음 규칙을 따르세요:
- 컬렉션에는 복수 명사 사용:
/users,/products,/orders - 하이픈과 함께 소문자 사용:
/user-profiles,/userProfiles나/user_profiles가 아님 - 관련 리소스를 논리적으로 중첩:
/users/123/orders/456 - 중첩 깊이 제한: 2-3 레벨을 넘어서면 쿼리 매개변수나 별도 엔드포인트 사용
- 파일 확장자 피하기:
/users,/users.json이 아님 (대신 Accept 헤더 사용) - 데이터베이스 ID가 아닌 리소스 ID 사용: 공개 식별자로 UUID나 슬러그 고려
비리소스 작업 처리
때로는 리소스 모델에 깔끔하게 맞지 않는 작업을 노출해야 합니다. 이러한 경우 작업 자체를 리소스로 취급하세요:
POST /users/123/password-reset
POST /orders/456/cancellation
POST /reports/generate
GET /search?q=laptop&category=electronics
이러한 엔드포인트는 동작이나 프로세스를 나타내며, 대안이 어색한 리소스 매핑을 강제하는 것일 때 허용됩니다.
빠른 팁: URL을 설계할 때 새로운 개발자에게 설명한다고 상상해보세요. URL이 특정 방식으로 구조화된 이유를 설명하는 데 한 문장 이상이 필요하다면 아마도 너무 복잡한 것입니다.
HTTP 메서드와 상태 코드
HTTP는 작업을 설명하기 위한 풍부한 어휘를 제공합니다. 메서드와 상태 코드를 올바르게 사용하면 API를 예측 가능하게 만들고 캐싱, 디버깅, 표준 HTTP 도구와의 통합이 더 쉬워집니다.
HTTP 메서드
| 메서드 | 동작 | 성공 코드 | 멱등성 | 안전성 |
|---|---|---|---|---|
GET |
리소스 검색 | 200 OK | 예 | 예 |
POST |
새 리소스 생성 | 201 Created | 아니오 | 아니오 |
PUT |
전체 리소스 대체 | 200 OK / 204 No Content | 예 | 아니오 |
PATCH |
부분 업데이트 | 200 OK | 아니오* | 아니오 |
DELETE |
리소스 제거 | 204 No Content | 예 | 아니오 |
HEAD |
헤더만 가져오기 | 200 OK | 예 | 예 |
OPTIONS |
허용된 메서드 가져오기 | 200 OK | 예 | 예 |
*PATCH는 멱등성을 가지도록 설계할 수 있지만 사양에서 보장되지는 않음
멱등성 이해하기
멱등 작업은 몇 번을 실행하든 동일한 결과를 생성합니다. 이 속성은 네트워크 장애로 인해 재시도가 발생할 수 있는 분산 시스템의 신뢰성에 중요합니다.
GET /users/123은 멱등성이 있습니다 — 한 번 호출하든 100번 호출하든 동일한 사용자 데이터를 반환합니다. DELETE /users/123도 멱등성이 있습니다 — 첫 번째 호출은 사용자를 삭제하고, 후속 호출은 404를 반환하지만 최종 상태는 동일합니다.
POST /users는 멱등성이 없습니다 — 각 호출은 새 사용자를 생성합니다. 멱등 생성이 필요한 경우 클라이언트 생성 ID와 함께 PUT을 사용하거나 멱등성 키를 구현하세요.
필수 상태 코드
200과 500만 사용하지 마세요. 적절한 상태 코드는 클라이언트가 응답 본문을 파싱하지 않고도 응답을 올바르게 처리하는 데 도움이 됩니다.
성공 코드 (2xx):
200 OK— 본문이 있는 표준 성공 응답201 Created— 리소스가 성공적으로 생성됨 (Location 헤더 포함)204 No Content— 응답 본문이 없는 성공 (DELETE에 일반적)202 Accepted— 비동기 처리를 위해 요청이 수락됨
클라이언트 오류 코드 (4xx):
400 Bad Request— 잘못된 요청 구문 또는 유효성 검사 실패401 Unauthorized— 인증 필요 또는 실패403 Forbidden— 인증되었지만 권한 없음404 Not Found— 리소스가 존재하지 않음409 Conflict— 요청이 현재 상태와 충돌 (예: 중복 이메일)422 Unprocessable Entity— 올바른 형식의 요청에 대한 유효성 검사 오류429 Too Many Requests— 속도 제한 초과
서버 오류 코드 (5xx):
500 Internal Server Error— 일반 서버 오류502 Bad Gateway— 업스트림 서버의 잘못된 응답503 Service Unavailable— 일시적 과부하 또는 유지보수504 Gateway Timeout— 업스트림 서버 시간 초과
프로 팁: 429 및 503 응답에는 항상 Retry-After 헤더를 포함하여 클라이언트가 언제 재시도할 수 있는지 알려주세요. 이렇게 하면 서비스가 복구될 때 썬더링 허드 문제를 방지할 수 있습니다.
오류 응답 설계
오류 응답은 많은 API가 부족한 부분입니다. 불명확한 오류 메시지는 5분 수정을 몇 시간의 디버깅으로 바꿀 수 있습니다. 오류 응답은 일관되고, 유익하며, 실행 가능해야 합니다.
표준 오류 형식
모든 오류 응답에서 일관된 구조를 사용하세요:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "요청에 잘못된 데이터가 포함되어 있습니다",
"details": [
{
"field": "email",
"message": "이메일 주소가 이미 등록되어 있습니다",
"code": "DUPLICATE_EMAIL"
},
{
"field": "password",
"message": "비밀번호는 최소 8자 이상이어야 합니다",
"code": "PASSWORD_TOO_SHORT"
}
],
"request_id": "req_7f8a9b2c3d4e5f6g",
"documentation_url": "https://api.example.com/docs/errors/validation"
}
}
오류 응답 구성 요소
- 기계 판독 가능 코드: 클라이언트가 프로그래밍 방식으로 처리할 수 있는 일관된 오류 코드 사용
- 사람이 읽을 수 있는 메시지: 최종 사용자에게 표시하기에 적합한 명확한 설명
- 필드 수준 세부 정보: 유효성 검사 오류의 경우 어떤 필드가 실패했고 그 이유를 명시
- 요청 ID: 지원 및 디버깅을 위한 고유 식별자 포함
- 문서 링크: 해결 단계에 대한 관련 문서 링크
유효성 검사 오류 모범 사례
발견된 첫 번째 오류만이 아니라 모든 유효성 검사 오류를 한 번에 반환하세요. 개발자가 한 오류를 수정한 후 다른 오류를 발견하는 두더지 잡기 게임을 하지 않아야 합니다.
POST /users
{
"email": "invalid-email",
"password": "123",
"age": -5
}
응답: 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "요청 유효성 검사 실패",
"details": [
{
"field": "email",
"message": "유효한 이메일 주소여야 합니다",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "최소 8자 이상이어야 합니다",
"code": "TOO_SHORT"
},
{
"field": "age",
"message": "양수여야 합니다",
"code": "INVALID_VALUE"
}
]
}
}
보안 고려 사항
오류 메시지에서 민감한 정보가 유출되지 않도록 주의하세요. 사용자 계정이 존재하는지 여부를 밝히거나, 내부 시스템 세부 정보를 노출하거나, 프로덕션에서 스택 추적을 제공하지 마세요.
대신: "사용자 [email protected]을 찾을 수 없습니다"
사용: "잘못된 이메일 또는 비밀번호"
서버 측에서 자세한 오류 정보를 로깅하되 클라이언트에는 정제된 메시지를 반환하세요.
페이지네이션과 필터링
단일 응답으로 수천 개의 레코드를 반환하면 성능이 저하되고 사용자 경험이 나빠집니다. 페이지네이션은 컬렉션을 반환하는 모든 엔드포인트에 필수적입니다.
페이지네이션 전략
오프셋 기반 페이지네이션은 간단하고 익숙합니다:
GET /users?limit=20&offset=40
응답:
{
"data": [...],
"pagination": {
"limit": 20,
"offset": 40,
"total": 1247,
"has_more": true
}
}
장점: 구현이 쉽고, 임의의 페이지로 이동 지원
단점: 큰 오프셋에서 성능 저하, 요청 간 데이터 변경 시 일관성 없는 결과
커서 기반 페이지네이션은 대규모 데이터셋에 더 견고합니다:
GET /users?limit=20&cursor=eyJpZCI6MTIzNDU2fQ
응답:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIzNDc2fQ",
"has_more": true
}
}
장점: 일관된 결과, 더 나은 성능, 실시간 데이터 처리
단점: 임의의 페이지로 이동 불가, 구현이 약간 더 복잡
페이지 기반 페이지네이션은 UI에 사용자 친화적입니다:
GET /users?page=3&per_page=20
응답:
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total_pages": 63,
"total_items": 1247
}
}
필터링과 정렬
클라이언트가 쿼리 매개변수를 사용하여 결과를 필터링하고 정렬할 수 있도록 허용하세요:
GET /users?status=active&role=admin&sort=-created_at,name
일반적인 패턴:
- 동등성:
?status=active - 비교:
?age_gt=18&age_lt=65