C.W.K.
Stream
Lesson 02 of 07 · published

URI 설계 — 명사, 복수, 살려주는 규칙

~10 min · rest-design, uri-design, naming

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"URI 는 API 계약의 가장 작고 가장 보이는 조각. 로그, dashboard, docs, 청구 보고서, 고객 에러 보고에 나타나. 제대로 해; 후회 안 함. 잘못 해; 몇 년 같이 살아."

결정 95% 커버하는 여섯 규칙

1. 명사, 동사 아님. URI 가 action 아닌 thing 이름. /users/42/getUser/42 아님. Verb 는 HTTP method. GET /users/42 가 이미 "user 42 줘" 라고 함; GET /getUser/42 가 두 번 말하고 protocol verb 헷갈리게 함.

2. Collection 은 복수, item 은 단수. Collection 은 thing set: /users. Item 은 그 set 안 특정 thing: /users/{id}. Collection 에 복수 고수해 — /user/42 가 이상하게 읽히고 일관성 깸.

3. Nesting 은 ownership 반영, relationship 아님. /users/{id}/orders 는 "이 user 에 속한 order" 의미. 관계가 many-to-many 거나 ownership 의미 안 하면 top-level resource 와 query param 써: /orders?user_id={id}/users/{id}/orders 보다.

4. 소문자, 단어 사이 hyphen. /account-settings/{id}/AccountSettings/{id}/account_settings/{id} 아님. URL 이 대부분 context 에서 대소문자 구분; 소문자가 모호함 제거. Hyphen 이 underscore 이김 — underscore 가 hyperlink 아래 시각적으로 숨겨짐 (account_settings vs account-settings 보면).

5. 파일 확장자 없음. /users/42/users/42.json 아님. Format 은 Content-Type 에 속하지 URI 아님. URI 의 .json 이 representation 을 resource identity 에 coupling, 정확히 content negotiation 이 피하려고 존재하는 거.

6. 2-3 레벨 이상 nest 마. /users/{u}/orders/{o}/items/{i} 가 edge. /users/{u}/orders/{o}/items/{i}/comments/{c}/replies/{r} 는 설계 멈추고 API 와 논쟁 시작 신호. 안정 ID 가질 때 깊은 resource 를 top-level 로 promote 해서 평탄화 (/replies/{r}).

ID 선택 — Opaque 가 sequential 이김

흔한 ID 전략 둘:

  • Sequential integer (/users/1, /users/2): 싸고, 정렬 가능, debug 가능. ID 증가 보는 누구든 성장률 누설. 열거 쉬움 (/users/1, /users/2, ...).
  • Opaque string (/users/usr_8x3kPq, UUID, ULID, Stripe-스타일 prefixed ID): 열거 안 됨, 성장률 누설 없음, 로그에서 grep 쉬움. 약간 덜 debug 가능 (외우기 어려움).

사용자 대면이나 공개 거면 opaque 선호. Stripe 의 prefix convention (customer 에 cus_, payment 에 pay_) 이 금 — ID 혼동 불가능. 내부 전용 ID 는 integer 유지 가능.

Action URI — 규칙 1 의 유일한 예외

진짜로 resource 아닌 operation 있을 때 convention 은 POST /resources/{id}/actionName. POST /payments/42/refund, POST /jobs/abc/cancel. Action 이름이 URI 에 나타나 — 쓸 명사 없어서. 이게 Lesson 3.1 의 하이브리드 패턴 — API 의 resource-oriented 나머지와 일관.

URI 는 영원. URI 가 production 로그, 고객 통합 코드, 제 3자 docs, 브라우저 북마크, 검색 엔진 index 에 들어가면, 변경에 몇 년 걸리는 migration 필요. URI 설계에 처음 한 시간 더 써; 가장 싼 한 시간 될 거야.

Bad → Good 걸어쓰기

흔한 smell 다섯과 fix:

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 선택

cwkPippa URI 가 이 규칙 따름 — 복수 collection, opaque 대화 ID (UUID), hyphen 가진 소문자, 파일 확장자 없음, max 2-3 레벨 nesting. 주목할 예외: POST /api/council/{id}/finalizePOST /api/heartbeat/cron/{id}/run-now 같은 action endpoint, action 이지 resource 아니라서 의도적으로 verb-suffix 씀. 그 의도적 예외 괜찮음; 추가하는 bar 는 "이 operation 에 진짜 명사 없어?".

Code

동작하는 어휘 — 여섯 규칙 동작·text
# Resource-oriented URI 어휘 — 이 template 복사

# Collection (복수)
GET    /users                                # 나열
POST   /users                                # 생성

# Item (collection 안 단수)
GET    /users/{id}                           # 읽기
PUT    /users/{id}                           # 교체
PATCH  /users/{id}                           # 부분 update
DELETE /users/{id}                           # 제거

# Nested ownership (max 2-3 레벨)
GET    /users/{id}/orders                    # user 에 속한 order
POST   /users/{id}/orders                    # user 아래 order 생성
GET    /users/{id}/orders/{oid}              # 특정 order 읽기

# Sub-collection (깊은 nesting 피해)
GET    /orders/{oid}/items                   # OK
GET    /orders/{oid}/items/{iid}             # OK (3 깊이, edge)
GET    /comments/{cid}                       # 승격, /orders/{o}/items/{i}/comments/{c} 아님

# Action endpoint (path 에 verb, POST method, 이름 붙은 action)
POST   /payments/{id}/refund
POST   /jobs/{id}/cancel
POST   /messages/{id}/forward
FastAPI: 복수 collection, opaque ID, 합리적 nesting, action endpoint·python
# 여섯 규칙 다 따르는 FastAPI route
from fastapi import FastAPI, APIRouter, status

app = FastAPI()

# 최상위 collection: /users
users = APIRouter(prefix='/users', tags=['users'])

@users.get('')
async def list_users():
    return await query_users()

@users.post('', status_code=status.HTTP_201_CREATED)
async def create_user(payload: dict):
    new_id = await create(payload)  # opaque ID, 'usr_8x3kPq' 같은
    return {'id': new_id, **payload}

@users.get('/{uid}')
async def read_user(uid: str):
    return await get(uid)

# Nested: /users/{uid}/orders
@users.get('/{uid}/orders')
async def list_user_orders(uid: str):
    return await orders_for_user(uid)

# 승격 (3 레벨 이상 안 감): /orders/{oid}
orders = APIRouter(prefix='/orders', tags=['orders'])

@orders.get('/{oid}')
async def read_order(oid: str):
    return await get_order(oid)

# Action endpoint — path 에 verb-name, POST method
@orders.post('/{oid}/cancel', status_code=status.HTTP_202_ACCEPTED)
async def cancel_order(oid: str):
    return await cancel(oid)

app.include_router(users)
app.include_router(orders)

External links

Exercise

이 잘못 설계된 URI 목록 받고 여섯 규칙 따르도록 각각 다시 써: (1) POST /createUser, (2) GET /User_Profile/42.json, (3) POST /user/42/delete, (4) GET /getOrdersForUser?userId=42, (5) PUT /modify-order/abc/status?to=cancelled, (6) GET /users/42/orders/abc/items/1/comments/9/replies/3. 각각에 원본이 어느 규칙 위반했는지 설명.
Hint
(1) POST /users. (2) GET /users/42 with Accept: application/json. (3) DELETE /users/42. (4) GET /users/42/orders. (5) PATCH /orders/abc with {status: 'cancelled'} 혹은 POST /orders/abc/cancel. (6) 승격: GET /replies/3 (안정 ID 있으면 nesting 멈춤). 규칙 위반: path 에 verb (1, 3, 4, 5), 대소문자 + 확장자 (2), underscore (2), method 와 redundant verb (3), action-as-query-param (5), deep nesting (6).

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.