2년 이상 된 코드베이스를 열어보면 /getUser, /user_list, /Users/fetchAll, 세 가지 다른 페이지네이션 스키마, 그리고 customerID 필드가 order_id 옆에 동일한 응답으로 앉아있는 흉터를 발견할 것입니다. 그 어떤 것도 당장 고장을 일으키지는 않지만, 이 모든 것이 모두를 느리게 만듭니다. 이름 지정은 가장 저렴한 API 설계 결정이지만, 되돌리기에는 가장 비쌉니다. 클라이언트가 /getOrders에 의존하게 되면, 수년간 이를 지원해야 합니다. 이 가이드는 REST API가 강요하는 모든 이름 지정 결정에 대해 구체적인 규칙을 제시하고, 각 규칙에 대한 예시와 반례를 제공합니다. 이는 더 광범위한 저희의 개발자를 위한 REST API 가이드라인과 동일한 사고방식을 따르지만, 팀들이 가장 많이 논쟁하는 부분, 즉 무엇을 어떻게 부를지에 초점을 맞춥니다. 코드 검토 댓글 대신 도구를 사용하여 이러한 규칙을 적용하고 싶다면, Apidog를 통해 누군가 코드를 작성하기 전에 모든 엔드포인트를 공유 스키마에 대해 시각적으로 정의할 수 있습니다. 자세한 내용은 마지막에 설명합니다.
컬렉션에는 복수 명사를 사용하세요
URL은 리소스의 이름을 지정하며, 작업이 아닙니다. 컬렉션은 사물의 집합이므로 복수 명사로 이름을 지정하세요.
사용 권장:
GET /v1/products
GET /v1/products/89
GET /v1/orders
사용 지양:
GET /v1/getProducts
GET /v1/product
GET /v1/productList
복수 형태는 두 가지 레벨 모두에서 작동합니다. /products는 "제품 컬렉션"으로 읽히고, /products/89는 "컬렉션 내의 제품 89번"으로 읽힙니다. 단수 이름 지정은 하나의 항목에는 /product/89를 사용하고, 많은 항목에는 /product를 사용하는 등 어색한 URL을 강요하며, 이는 잘못된 방식으로 읽힙니다. Microsoft REST API 가이드라인은 정확히 이러한 이유로 복수 명사를 채택했으며, 대부분의 공개 API(Stripe, GitHub, Shopify)도 동일한 길을 따랐습니다.
한 가지 예외: 싱글톤 리소스. 사용자가 정확히 하나의 장바구니를 가지고 있다면 /users/42/cart는 괜찮습니다. 카디널리티가 1인 것은 복수화하지 마세요.
경로에서 동사를 제외하세요
HTTP 메서드가 동사입니다. 경로에 또 다른 동사를 넣는 것은 정보를 중복시키고 리소스 모델을 깨뜨립니다.
사용 권장:GET /v1/orders/42 (읽기) DELETE /v1/orders/42 (삭제하기) PATCH /v1/orders/42 (업데이트하기)
사용 지양:
GET /v1/fetchOrder/42
POST /v1/deleteOrder/42
POST /v1/updateOrderStatus
동사 기반 경로는 또한 표면적을 늘립니다. 네 가지 메서드를 가진 하나의 리소스는 별도로 문서화, 테스트, 캐싱해야 하는 네 가지 엔드포인트가 됩니다. 캐시 무효화도 더 나빠집니다. CDN은 GET /v1/orders/42를 캐시하고 DELETE /v1/orders/42에서 무효화할 수 있는데, 이는 둘 다 동일한 URL을 가리키기 때문입니다. /fetchOrder/42와 /deleteOrder/42는 연결할 수 없습니다.
URL 경로에 케밥 케이스(kebab-case)를 사용하세요
다중 단어 경로 세그먼트에는 구분 기호가 필요하며, 하이픈이 올바른 구분 기호입니다.
사용 권장:
/v1/gift-cards
/v1/shipping-addresses
사용 지양:
/v1/giftCards
/v1/gift_cards
/v1/GiftCards
세 가지 이유가 있습니다. Google은 하이픈을 인덱싱을 위한 단어 구분 기호로 취급하므로 공개 API 문서는 케밥 케이스를 사용하면 순위가 더 높습니다. 밑줄은 이메일이나 문서에서 URL에 밑줄이 그어질 때 사라집니다. 그리고 URL의 카멜 케이스(camelCase)는 대소문자 구분 버그를 유발합니다. /giftCards와 /giftcards는 대부분의 서버에서 다른 URL이며, 누군가는 잘못된 것을 입력할 것입니다. Zalando RESTful API 가이드라인은 케밥 케이스를 MUST 규칙으로 만들었으며, 그들은 수백 개의 내부 서비스에서 이 플레이북을 실행했습니다.
하나의 JSON 표기법을 선택하고 문서화하세요
요청 및 응답 본문 내 필드 이름의 경우, 솔직히 말하면 카멜 케이스와 스네이크 케이스(snake_case) 모두 작동합니다. 작동하지 않는 것은 이들을 혼용하는 것입니다.
사용 권장 (둘 중 하나를 일관되게):
{ "orderId": 42, "createdAt": "2026-08-30T09:15:00Z", "totalAmount": 4999 }
{ "order_id": 42, "created_at": "2026-08-30T09:15:00Z", "total_amount": 4999 }
사용 지양:
{ "orderId": 42, "created_at": "2026-08-30T09:15:00Z", "TotalAmount": 4999 }
카멜 케이스는 JavaScript 및 Java 클라이언트에 깔끔하게 매핑됩니다. 스네이크 케이스는 스캔하기 쉽고 Ruby, Python, 대부분의 SQL 열 이름과 일치합니다. Stripe는 모든 곳에서 스네이크 케이스를 사용합니다. API를 가장 많이 사용하는 소비자를 기준으로 선택한 다음, 스타일 가이드에 선택 사항을 기록하여 모든 풀 리퀘스트에서가 아닌 한 번만 논쟁이 발생하도록 하세요. 혼합된 표기법은 서로 다른 팀이 다른 엔드포인트를 제공하기 때문에 실제 API에서 가장 흔한 불일치입니다. 이는 취향의 실패가 아니라 거버넌스의 실패입니다.
중첩은 두 단계로 제한하세요
중첩은 소유권을 표현합니다. /users/42/orders는 "사용자 42에 속하는 주문"을 의미합니다. 이는 유용합니다. 두 단계를 넘어서면 유용성이 떨어집니다.
사용 권장:
GET /v1/users/42/orders
GET /v1/orders/1337/refunds
사용 지양:
GET /v1/users/42/orders/1337/refunds/7/status
깊은 중첩은 리프(leaf) 리소스가 자체적으로 전역적으로 고유한 ID를 가지고 있더라도 클라이언트가 모든 조상 ID를 전달해야만 리프 리소스에 도달할 수 있도록 강요합니다. 환불에 ID 7이 있다면, /refunds/7 또는 /orders/1337/refunds/7으로 노출하고 거기서 멈추세요. 좋은 냄새 테스트: URL에 세 개 이상의 ID가 포함되어 있다면 평면화하세요. 주문이 일단 존재하면 경로에 사용자가 필요하지 않습니다. /orders/1337은 독립적으로 존재할 수 있습니다.
필터링, 정렬, 페이지네이션은 쿼리 파라미터에 넣으세요
경로는 리소스를 식별합니다. 쿼리 파라미터는 리소스를 보는 방식을 수정합니다. 경로에 필터를 인코딩하지 마세요.
사용 권장:
GET /v1/orders?status=active&sort=-created_at&limit=50&cursor=eyJpZCI6NDJ9
GET /v1/products?category=electronics&min_price=1000
사용 지양:
GET /v1/orders/active
GET /v1/orders/sorted-by-date-desc
GET /v1/getOrdersByStatusAndDate
sort=-created_at 패턴(내림차순 정렬을 위한 마이너스 접두사)은 JSON:API 사양에서 비롯되었으며, 두 번째 order=desc 파라미터를 절약합니다. /orders/active와 같은 필터 경로는 필터를 결합해야 할 때까지는 무해해 보이지만, 그 후에는 조합당 새로운 엔드포인트를 만들어야 합니다. 페이지네이션 파라미터 이름도 동일한 규율이 필요합니다. limit/cursor 또는 page/per_page를 한 번 선택하고 모든 컬렉션에서 재사용하세요. 저희의 API 페이지네이션 가이드는 커서-오프셋 트레이드오프를 심층적으로 다루고 있으며, 여기서의 이름 지정 규칙은 단순히 통일성을 유지하는 것입니다.
경로에 버전을 포함하세요
주류 옵션은 두 가지입니다: 경로 세그먼트(/v1/products) 또는 헤더(Accept: application/vnd.myapi.v1+json). 헤더 버전 관리는 URL이 버전 간에 동일한 리소스를 계속 이름 지정하므로 더 "순수한" REST이며, Google API 설계 지침은 두 접근 방식 모두 널리 사용되고 있음을 언급합니다. 그러나 경로 버전 관리는 운영상의 이점으로 우수합니다. 모든 로그 라인에서 보이고, 브라우저에서 테스트 가능하며, Vary 헤더 없이 캐시 가능하고, 클라이언트가 잊을 수 없습니다. 누락된 버전 헤더로 인해 "curl에서는 작동하지만, 운영 환경에서는 실패하는" 문제를 디버깅해 본 모든 개발자는 다른 대안의 비용을 알고 있습니다. /v1/을 사용하되, 주 버전만 사용하고 /v1.2/는 사용하지 마세요. 사소한 변경은 추가적이고 비호환적이지 않아야 합니다. 콘텐츠 협상을 포함한 전체 의사 결정 트리는 저희의 API 버전 관리 전략 비교를 참조하세요.
리소스 ID를 불투명하게 처리하고, 순차적인 정수를 부주의하게 노출하지 마세요
/orders/41, /orders/42, /orders/43: 순차적인 정수 ID는 당신이 처리하는 주문 수를 보는 모든 사람에게 정확히 알려주며, 공격자가 ID 공간을 탐색하여 권한 부여의 허점을 찾는 열거형 공격을 유발합니다. 이러한 종류의 버그, 즉 깨진 객체 수준 권한 부여는 OWASP API Security Top 10에서 1위를 차지합니다.
사용 권장:
GET /v1/orders/ord_9f8e2a71b3
GET /v1/users/550e8400-e29b-41d4-a716-446655440000
사용 지양 (열거가 중요한 경우):
GET /v1/orders/42
GET /v1/invoices/10883
Stripe의 ord_9f8e2a71b3와 같은 접두사가 붙은 무작위 ID는 가장 강력한 패턴입니다. 추측 불가능하고, 로그에서 자체 설명적이며, 노출해도 안전합니다. 어느 쪽이든 권한 부여 검사는 여전히 필수입니다. 불투명한 ID는 누락된 검사의 폭발 반경을 줄일 뿐, 검사를 대체하지는 않습니다. 내부적으로는 정수 기본 키를 유지할 수 있습니다. 이 규칙은 URL에 노출하는 것에 관한 것입니다.
비-CRUD 작업을 컨트롤러 리소스로 모델링하세요
결국에는 깔끔한 CRUD 매핑이 없는 작업이 필요할 것입니다. 주문 취소, 결제 재시도, 이메일 재전송 등. 상태 필드에 대한 PATCH를 통해 터널링하거나, 최상위 레벨에 동사를 넣지 마세요.
사용 권장:
POST /v1/orders/42/cancel
POST /v1/payments/pay_88a1/retry
사용 지양:
PATCH /v1/orders/42 { "status": "cancelled" }
POST /v1/cancelOrder { "orderId": 42 }
이것이 컨트롤러 패턴이며, 동사 금지 규칙에 대한 유일한 허용된 예외입니다. 동사는 경로의 끝에, 해당 작업이 적용되는 리소스 아래에 범위가 지정됩니다. PATCH 접근 방식은 RESTful처럼 보이지만, 상태 필드 업데이트 내부에 상태 머신을 숨깁니다. 주문 취소는 환불을 유발하고, 재고를 해제하며, 알림을 보냅니다. 이를 필드 쓰기인 것처럼 가장하는 것은 서버가 의도를 감지하기 위해 페이로드를 비교하도록 강요합니다. /cancel 엔드포인트는 의도를 명확히 하고, 작업에 자체 권한과 감사 추적을 부여하며, 취소 사유와 같은 작업별 입력에 대한 여지를 남깁니다.
헤더와 쿼리 파라미터의 표기법을 일관되게 유지하세요
두 가지 작은 표면이지만, 동일한 규율이 적용됩니다. 사용자 정의 헤더는 HTTP 규칙에 맞춰 하이픈-파스칼-케이스(Hyphenated-Pascal-Case)를 사용합니다. Idempotency-Key, Request-Id. 오래된 X- 접두사는 건너뛰세요. 이는 2012년 RFC 6648에 의해 폐기되었습니다. 헤더 이름은 전송 시 대소문자를 구분하지 않지만, 문서와 SDK는 여전히 한 가지 방식으로 철자를 표기해야 합니다.
쿼리 파라미터는 JSON 본문 표기법과 일치해야 합니다. 본문이 스네이크 케이스를 사용한다면, ?min_price=1000&created_after=2026-01-01로 작성하고, ?minPrice=1000으로는 작성하지 마세요. 응답에서 created_at을 읽고 쿼리에서 createdAfter를 입력해야 하는 개발자는 처음에는 잘못 입력할 것이며, 그 이후의 모든 사람도 마찬가지일 것입니다.
전체 규칙 요약
| # | 규칙 | 권장 | 지양 |
|---|---|---|---|
| 1 | 컬렉션에는 복수 명사 사용 | /products, /products/89 |
/getProducts, /productList |
| 2 | 경로에 동사 없음 | DELETE /orders/42 |
POST /deleteOrder/42 |
| 3 | 케밥 케이스 경로 세그먼트 | /gift-cards |
/giftCards, /gift_cards |
| 4 | 하나의 JSON 표기법, 문서화 | 모든 곳에서 order_id |
orderId와 order_id 혼용 |
| 5 | 최대 두 단계 중첩 | /orders/1337/refunds |
/users/42/orders/1337/refunds/7 |
| 6 | 쿼리 파라미터에 필터 및 페이지네이션 | ?status=active&sort=-created_at |
/orders/active |
| 7 | 경로에 주 버전 | /v1/products |
/v1.2/products, 버전 헤더 |
| 8 | 불투명한 리소스 ID | /orders/ord_9f8e2a71b3 |
/orders/42 (공개, 열거 가능) |
| 9 | 작업에 대한 컨트롤러 패턴 | POST /orders/42/cancel |
PATCH ({"status":"cancelled"} 포함) |
| 10 | 헤더 및 파라미터 표기법 일관성 | Idempotency-Key, ?min_price= |
X-IDEMPOTENCY_KEY, ?minPrice= 혼용 |
규모에 맞춰 컨벤션 적용하기
위키의 스타일 가이드는 아무것도 바꾸지 않습니다. API를 일관되게 유지하는 팀은 한 가지 습관을 공유합니다. 코드가 존재하기 전에 먼저 설계하고 컨벤션을 적용하는 것입니다. 이것이 실제로 API 거버넌스의 핵심입니다.
여기서 Apidog가 워크플로우에서 그 자리를 차지합니다. 엔드포인트는 스키마 우선 시각 디자이너에서 정의되므로, 경로, 표기법 및 파라미터 이름은 컨트롤러 코드에 묻혀 있는 문자열이 아니라 명시적인 설계 아티팩트입니다. 공유 컴포넌트는 Pagination, Error, Money 스키마가 한 번 정의되고 모든 엔드포인트에서 재사용됨을 의미합니다. 아무도 새로운 서비스에서 per_page를 pageSize로 재발명하지 않습니다. 그리고 디자인은 검토 기능이 내장된 팀 작업 공간에 존재하므로, 리더는 디자인 단계에서 /getUserOrders와 같은 문제를 포착할 수 있습니다. 이 시점에서는 이름 변경에 한 번의 클릭 비용만 들지만, 세 명의 클라이언트가 통합한 후에는 훨씬 더 큰 비용이 듭니다. 스펙은 문서, 목업 서버 및 테스트를 구동하므로, 승인된 이름이 모든 사람이 배포하는 이름이 됩니다. Apidog를 다운로드하여 다음 새 엔드포인트에 무료로 사용해 보세요. 오래된 API를 개선하는 것은 어렵지만, 새로운 API에 대한 기준을 지키는 것은 어렵지 않습니다.
FAQ
REST URL은 복수여야 할까요, 단수여야 할까요?
인스턴스가 두 개 이상인 모든 리소스에는 복수형을 사용하세요. /products, /orders, /users. 복수형은 컬렉션(/orders)과 개별 멤버(/orders/42) 모두에 자연스럽습니다. /users/42/cart와 같은 진정한 싱글톤에만 단수 이름을 예약하세요. 리소스 모델링의 더 깊은 추론을 원하시면, REST API란 무엇인가에 대한 저희 가이드가 기본 원칙부터 자세히 설명합니다.
JSON 필드 이름에 카멜 케이스 또는 스네이크 케이스 중 어느 것이 더 좋을까요?
어느 쪽도 우월하지 않습니다. 카멜 케이스는 JavaScript를 주로 사용하는 소비자에게 적합하며, 스네이크 케이스는 더 읽기 쉽고 Python, Ruby, Stripe의 공개 API와 일치합니다. 중요한 규칙: 하나를 선택하고, 스타일 가이드에 기록하며, 스키마 검토에서 이를 적용하세요. 여러 엔드포인트에서 표기법을 혼용하는 것이 어떤 선택보다 더 큰 해를 끼칩니다.
API 버전을 URL에 넣어야 할까요, 아니면 헤더에 넣어야 할까요?
강력한 하이퍼미디어 요구 사항이 없는 한 경로(/v1/orders)를 사용하세요. 경로 버전은 로그, 캐시 및 브라우저 테스트에 클라이언트의 노력 없이 나타납니다. 헤더 버전 관리는 버전 간에 URL을 안정적으로 유지하지만, 클라이언트가 헤더를 잊었을 때 조용히 실패합니다. 주 버전만 사용하세요. 사소한 변경은 추가적이고 호환성을 깨뜨리지 않는 업데이트로 제공하세요.
REST API 경로에 동사가 허용되는 경우가 있나요?
네, 한 곳에서는 허용됩니다. POST /orders/42/cancel 또는 POST /payments/pay_88a1/retry와 같은 비-CRUD 작업에 대한 컨트롤러 엔드포인트입니다. 동사는 경로의 끝에, 해당 리소스 아래에 범위가 지정되며, 메서드는 항상 POST입니다. 다른 모든 곳에서는 HTTP 메서드가 동사를 전달하고 경로는 명사만 유지합니다.
