"URI는 API 계약에서 가장 작으면서도 가장 자주 노출되는 조각이야. 로그, 관리 화면, 문서, 청구 보고서, 오류 제보에 계속 나타나. 잘 지으면 오래 편하고, 성급하게 지으면 몇 년 동안 그 선택을 끌고 가야 해."
대부분의 결정을 정리하는 여섯 가지 기본값
1. 기본은 명사, 동작은 HTTP 메서드로 표현해. URI에는 수행할 명령보다 다루는 대상을 놓는 편이 좋아. /users/42가 /getUser/42보다 간결해. GET /users/42만으로 읽기 의미가 이미 드러나므로 GET /getUser/42처럼 동사를 되풀이할 필요가 없어.
2. 컬렉션 이름은 한 가지 수 체계로 통일해. 이 과정에서는 컬렉션을 복수형으로 써. /users는 사용자 집합이고, /users/{id}는 그 집합의 특정 항목이야. 단수형도 기술적으로 가능하지만 /user/42와 복수형 컬렉션을 섞으면 소비자가 매번 이름을 외워야 해.
3. 중첩은 소유나 명확한 범위를 드러낼 때만 써. /users/{id}/orders는 특정 사용자의 주문이라는 범위를 자연스럽게 보여 줘. 다대다 관계이거나 부모를 거치지 않아도 독립적으로 식별되는 리소스라면 최상위 컬렉션과 필터를 쓰는 편이 나을 수 있어. 예를 들어 /orders?user_id={id}와 /users/{id}/orders 가운데 도메인 의미와 권한 경계에 맞는 쪽을 골라.
4. 소문자와 하이픈을 일관된 관례로 삼아. /account-settings/{id}처럼 쓰면 /AccountSettings/{id}나 /account_settings/{id}와 섞일 일이 없어. URI 경로의 대소문자 처리는 서버와 경로에 따라 구별될 수 있으므로 소문자로 통일하는 편이 안전해. 하이픈은 링크 밑줄과 겹쳐 보일 수 있는 언더스코어보다 읽기 편한 경우가 많아(account_settings와 account-settings를 비교해 봐).
5. 표현 형식은 보통 경로에서 분리해. 이 API에서는 /users/42를 쓰고 /users/42.json은 쓰지 않아. 표현 형식은 Content-Type과 콘텐츠 협상으로 다루면 같은 리소스 식별자를 유지할 수 있어. URI의 .json 같은 확장자가 프로토콜상 금지된 것은 아니지만, 여러 표현을 제공할 API라면 URI에 형식을 고정하지 않는 편이 유연해.
6. 깊은 중첩은 경고 신호로 봐. /users/{u}/orders/{o}/items/{i} 정도만 되어도 클라이언트가 알아야 할 조상 정보가 많아져. /users/{u}/orders/{o}/items/{i}/comments/{c}/replies/{r}처럼 계속 깊어지면, 안정된 ID가 있는 리소스를 최상위로 올려 /replies/{r}처럼 평평하게 접근할 수 있는지 검토해. 두세 단계는 법칙이 아니라 복잡도를 다시 살펴볼 실용적 기준이야.
ID 선택 — 공개 식별자에는 예측하기 어려운 값을 고려해
흔히 쓰는 ID 전략은 크게 둘이야:
- 순차 정수(
/users/1,/users/2): 생성 비용이 낮고 사람이 읽고 디버깅하기 쉬워. 반면 값의 증가 추세가 규모를 드러낼 수 있고, 인접 값을 추측하기도 쉬워(/users/1,/users/2, ...). - 불투명한 문자열(
/users/usr_8x3kPq, UUID, ULID, Stripe 스타일 접두사 ID): 값에서 순서나 규모를 추측하기 어렵고 분산 환경에서 발급하기 편할 수 있어. 대신 사람이 외우거나 직접 입력하기는 어렵고, 형식에 따라 길이와 인덱스 비용도 달라져.
공개 API라면 예측하기 어려운 ID를 우선 검토해. Stripe의 접두사 관례는 고객에 cus_, 결제에 pay_를 붙여 로그에서 타입을 빠르게 구분하게 해. 다만 불투명한 ID는 권한 검사를 대신하지 않아. 모든 리소스 접근에는 별도의 인증과 인가가 필요하고, 내부 전용 시스템에서는 순차 정수가 더 단순한 선택일 수 있어.
동작 URI — 명사만으로 뜻이 흐려질 때
리소스의 일반적인 생성·조회·수정·삭제로 자연스럽게 표현하기 어려운 작업에는 POST /resources/{id}/actionName 같은 형태를 쓸 수 있어. POST /payments/42/refund, POST /jobs/abc/cancel이 그 예야. 첫 번째 형태는 문법을 설명하기 위한 도식이고, 실제 경로 이름은 앞서 정한 소문자·하이픈 관례에 맞춰. 동작을 숨기지 않으면서 API의 나머지 리소스 지향 구조도 유지하는 혼합형 패턴이야.
잘못된 예 → 더 나은 예로 고치기
아래는 이 API가 택한 관례에 맞춰 흔한 문제를 고친 예야:
BAD: GET /getUserById?id=42 (path 에 verb, ID 에 query)
GOOD: GET /users/42
BAD: POST /createOrder (path 에 verb)
GOOD: POST /orders
BAD: DELETE /users/42/delete (verb 가 method 와 중복)
GOOD: DELETE /users/42
BAD: GET /User/42.json (대소문자 + 확장자)
GOOD: GET /users/42 (Accept: application/json)
BAD: PATCH /user_settings_for/42 (snake + underscore + 어색)
GOOD: PATCH /users/42/settings
cwkPippa의 URI 선택
POST /api/council/{id}/finalize와 POST /api/heartbeat/cron/{id}/run-now처럼 기존 리소스에 명시적 작업을 수행하는 엔드포인트도 있어. 이런 예외는 의도가 분명하면 괜찮아. 새 동작 경로를 추가하기 전에는 "기존 리소스의 표준 메서드로 뜻을 더 명확하게 표현할 수 있는가?"를 먼저 물어봐.