리소스 경로와 HTTP 메서드가 연결된 구조를 통해 REST API URI 설계 원칙을 시각적으로 표현한 이미지입니다.
REST API 5일차: URI 설계 원칙과 베스트 프랙티스
한눈에 보는 요약
REST API에서 가장 먼저 마주치는 것이 바로 URI입니다. URI는 사용자가 “이 주소만 봐도 어떤 데이터인지 짐작할 수 있어야” 하고, 개발자는 “새 기능을 추가할 때도 같은 규칙으로 쉽게 확장”할 수 있어야 합니다.
이번 5일차에서는 앞선 글(REST API 디자인 기본 패턴)을 한 단계 더 확장해서, URI를 설계할 때 꼭 알아야 할 원칙과 실무에서 자주 쓰는 베스트 프랙티스를 예시 중심, 초보자도 이해하기 쉬운 방식으로 정리합니다.
목차
- 1. URI 설계가 중요한 이유
- 2. 좋은 URI의 기본 규칙
- 3. 경로(Path) vs 쿼리(Query) 언제 어떻게 쓸까
- 4. 계층 구조, 관계 표현, 버전·언어 설계
- 5. URI 설계 베스트 프랙티스 요약 표
- 6. 코드 예시: Express로 구현하는 URI 패턴
- 7. 내 프로젝트에 적용하는 단계별 가이드
- 8. 추가로 생각해볼 점
- 9. 블로그 최적화 정보
1. URI 설계가 중요한 이유
REST API는 결국 “리소스(데이터)를 URL로 표현하고, HTTP로 그 리소스를 다루는 방식”입니다. 즉, 사용자가 제일 먼저 보는 것은 문서가 아니라 URI 목록입니다.
- 가독성이 좋으면, 새 팀원이 와도 “아, 이 서비스 구조가 대충 이렇구나”를 빠르게 파악합니다.
- 일관성이 있으면, 기능을 추가할 때 기존 패턴을 그대로 따라 하기만 하면 됩니다.
- 유지보수성이 높아져서, 특정 리소스를 찾거나 변경해야 할 때 어디를 고쳐야 할지 직관적으로 떠오릅니다.
반대로 /doSomethingForUser, /getAllUserListAction처럼 동사 위주의 제각각인 URI는 처음에는 빠르게 구현돼 보이지만, 시간이 지날수록 개발 속도를 심하게 떨어뜨립니다.
2. 좋은 URI의 기본 규칙
2-1. URI는 명사, 메서드는 동사
REST에서 중요한 기본 원칙은 URI는 리소스를 나타내는 “명사”로, HTTP 메서드는 “동사”로 사용하는 것입니다.
- 좋은 예:
/users,/users/10,/orders/2024 - 좋지 않은 예:
/createUser,/getUsers,/deleteUserById
“사용자를 만든다”는 동작은 URI가 아니라 POST /users로 표현하고, “사용자 목록을 조회”하는 동작은 GET /users로 표현합니다.
2-2. 복수형 명사 사용
컬렉션(목록)을 표현할 때는 복수형 명사를 일관되게 사용하는 것이 일반적입니다.
/user(단수) 보다는/users(복수)/product보다는/products
특정 한 개의 리소스는 ID를 붙여서 표현합니다. 예: /users/1, /products/42
2-3. 소문자 + 하이픈(kebab-case)
단어가 길어질수록 /UserProfile, /user_profile보다 /user-profiles처럼 소문자+하이픈 패턴이 읽기 쉽고 URL에서도 가장 흔히 쓰입니다.
- 권장:
/user-profiles,/access-tokens - 비권장:
/userProfiles,/user_profiles,/UserProfiles
2-4. 의미 없는 기술 용어 피하기
유저 입장에서 의미가 없는 기술 용어는 가급적 URI에 넣지 않습니다.
- 비권장:
/tblUser,/userEntity,/userDto - 권장:
/users
2-5. 슬래시는 계층 구조만 표현
URI에서 슬래시(/)는 상위–하위(계층) 관계만 표현하도록 합니다.
/users/1/orders– “1번 사용자에 속한 주문들”이라는 관계를 자연스럽게 표현/users/1/orders/99– “1번 사용자의 99번 주문”
반대로 계층이 아닌 단순 필터 조건은 쿼리 파라미터에 두는 것이 더 좋습니다. 예: /orders?status=PAID
2-6. 끝 슬래시(Trailing Slash) 일관성
대부분의 API에서는 URI 끝에 슬래시를 붙이지 않는 스타일을 많이 사용합니다.
- 권장:
/users,/users/1 - 비권장:
/users/,/users/1/(혼합 사용 시 특히 문제)
중요한 것은 “어느 쪽이든 팀 규칙으로 정해서 일관되게 사용”하는 것입니다.
3. 경로(Path) vs 쿼리(Query) 언제 어떻게 쓸까
3-1. Path는 “리소스 위치”를, Query는 “조건”을
간단히 요약하면 아래처럼 생각하면 편합니다.
- Path – 무엇(어떤 리소스)을 가리키는지
- Query – 그 리소스를 “어떤 조건과 옵션으로” 가져올지
예시
GET /users/10– ID가 10인 사용자 하나GET /users?role=admin– 역할이 admin인 사용자 목록GET /users?active=true&page=1&size=20– 활성 유저를 1페이지 20개씩
3-2. Path에 들어가야 할 것들
- 리소스의 고유 식별자(ID)
- 상위–하위 관계를 나타내는 부모 리소스 ID
- 리소스 종류(컬렉션 이름)
좋은 예
/users/{userId}/users/{userId}/orders/{orderId}
3-3. Query에 들어가야 할 것들
- 검색 조건:
status=PAID,role=admin - 정렬 옵션:
sort=createdAt,desc - 페이징 옵션:
page=1,size=20 - 필터링 범위:
from=2024-01-01&to=2024-01-31
3-4. 리소스 vs 액션 URI
가능하면 URI는 리소스를 나타내게 하고, “행위적인 액션”은 메서드나 별도 개념으로 처리합니다.
- 비권장:
POST /users/1/block대신POST /users/1/blocks같은 리소스로 보는 방법도 있습니다. - 혹은
PATCH /users/1에서{ "blocked": true }처럼 상태를 변경하는 식으로 표현할 수 있습니다.
4. 계층 구조, 관계 표현, 버전·언어 설계
4-1. 계층 구조(Depth)를 너무 깊게 만들지 않기
중첩 URI는 관계를 표현하기 좋지만, /a/b/c/d/e처럼 깊어지면 관리가 어렵습니다.
- 적절:
/users/{id}/orders - 지나침:
/companies/{cid}/departments/{did}/users/{uid}/orders/{oid}
너무 깊어지는 경우, 상위 ID 일부는 쿼리 파라미터로 빼거나, 리소스 간 관계를 다른 방식(예: 토큰, 검색 조건)으로 해결하는 것도 고려해야 합니다.
4-2. 관계 표현 방식
대표적인 방식은 다음과 같습니다.
- 중첩 리소스:
/users/{id}/orders - 별도 검색:
/orders?userId={id}
어떤 방식을 택할지는 서비스 특성에 따라 다르지만, “주로 어떤 방향으로 데이터를 조회하는지”를 기준으로 정하면 좋습니다. 예를 들어 “사용자 → 주문” 방향 조회가 대부분이라면 /users/{id}/orders를 기본으로 두고, “여러 사용자 주문을 한 번에 검색”하는 기능은 /orders?userId=... 형태로 보완할 수 있습니다.
4-3. 버전 관리: /v1, /v2를 쓸까?
API가 커지면 언젠가 호환되지 않는 변경이 필요해집니다. 이때를 대비해 “버전 전략”을 미리 생각해두면 좋습니다.
- URL 버전:
/v1/users,/v2/users - 헤더 버전:
Accept: application/vnd.myapp.v1+json
학습·실습 단계나 작은 서비스에서는 URL 버전 방식이 가장 단순해서 많이 사용됩니다. 중요한 것은, 단순히 /v2를 붙이는 것이 아니라 “무엇이 어떻게 바뀌었는지”를 문서에 함께 기록하는 것입니다.
4-4. 언어/지역 설정
글로벌 서비스를 생각한다면 언어/지역에 대한 URI 정책도 필요합니다.
/ko/products,/en/products처럼 언어 코드를 Path에 넣는 방식Accept-Language헤더를 사용하는 방식
리소스 구조 자체는 같고, 내용(텍스트)만 번역된다면 보통 헤더로 처리하는 편이 깔끔합니다.
4-5. 파일 확장자, 슬러그, 인코딩
- 파일 확장자: 옛날 스타일의
/users.json보다는,Accept: application/json헤더로 응답 포맷을 협상하는 것이 REST스럽습니다. - Slug 사용:
/posts/123-what-is-rest-api같이 ID + 사람이 읽을 수 있는 제목을 함께 쓰면 SEO에 도움이 됩니다. - 인코딩: 공백·한글 등은 자동으로 인코딩되므로, URI 설계 시 “사용자가 직접 입력하지 않아도 될 정도로 간단한 구조”를 유지하는 것이 좋습니다.
5. URI 설계 베스트 프랙티스 요약 표
| 항목 | 권장 패턴 | 예시 | 비고 |
|---|---|---|---|
| 명사 중심 | 리소스 이름 사용 | /users, /orders | 동사는 HTTP 메서드로 표현 |
| 복수형 사용 | 컬렉션은 복수형 | /users, /products | 한 프로젝트에서 단·복수 혼용 금지 |
| 케밥 케이스 | 소문자 + 하이픈 | /user-profiles | 공백, 카멜케이스 지양 |
| 슬래시 의미 | 계층 구조만 표현 | /users/{id}/orders | 필터는 Query에 배치 |
| Path vs Query | Path: 리소스, Query: 조건 | /users/1, /users?active=true | 역할을 명확히 구분 |
| 버전 관리 | URL 또는 헤더 | /v1/users | 초기에는 URL 버전이 단순 |
| 관계 표현 | 필요 시 중첩 리소스 | /users/{id}/orders | 너무 깊은 중첩은 피하기 |
6. 코드 예시: Express로 구현하는 URI 패턴
이번에는 4일차에서 만든 사용자 API를 확장해, URI 설계 원칙을 반영한 예시 코드를 살펴보겠습니다.
// REST API URI 설계 예시 - Node.js + Express const express = require('express'); const app = express(); app.use(express.json()); // 1) /v1/users - 컬렉션 엔드포인트 app.get('/v1/users', (req, res) => { const { role, active, page = 1, size = 20 } = req.query; // 실제로는 DB 조회 로직이 들어갑니다. const users = [ { id: 1, name: 'Alice', role: 'admin', active: true }, { id: 2, name: 'Bob', role: 'user', active: true }, ]; // (예시를 단순화하기 위해 필터/페이징 로직은 생략) res.status(200).json({ items: users, page: Number(page), size: Number(size), totalCount: users.length, }); }); // 2) /v1/users/:id - 개별 리소스 app.get('/v1/users/:id', (req, res) => { const id = Number(req.params.id); if (id === 1) { return res.status(200).json({ id: 1, name: 'Alice' }); } return res.status(404).json({ code: 'USER_NOT_FOUND', message: '사용자를 찾을 수 없습니다.', }); }); // 3) /v1/users/:id/orders - 관계 표현 app.get('/v1/users/:id/orders', (req, res) => { const userId = Number(req.params.id); const orders = [ { id: 100, userId: 1, amount: 5000 }, { id: 101, userId: 1, amount: 12000 }, ]; const userOrders = orders.filter(o => o.userId === userId); res.status(200).json({ userId, items: userOrders, }); }); // 4) /v1/orders - Query로 필터링 app.get('/v1/orders', (req, res) => { const { userId, from, to } = req.query; // from, to 날짜 범위, userId 조건을 이용해 검색한다고 가정 res.status(200).json({ items: [], filter: { userId, from, to }, }); }); app.listen(3000, () => { console.log('API server listening on port 3000'); }); 위 예시에서는 다음과 같은 설계 원칙을 모두 사용하고 있습니다.
/v1을 붙여 URL 기반 버전 관리 적용/users,/orders등 복수형 명사 사용/users/:id/orders로 사용자-주문 관계를 계층 구조로 표현/orders?userId=1&from=...&to=...처럼 필터 조건은 쿼리 파라미터로 분리
7. 내 프로젝트에 적용하는 단계별 가이드
- 현재 URI 목록 작성하기
기존 프로젝트라면 포스트맨/문서/코드에서 사용하는 모든 URI를 표로 정리합니다.
예: 요청 메서드, URI, 의미, 예시 응답 코드 등. - 리소스 단위로 묶어 보기
화면이나 기능이 아니라 “유저, 주문, 게시글” 같은 리소스 기준으로 그룹화합니다. 그룹별로 기본 컬렉션 URI(/users,/orders)와 개별 리소스 URI(/users/{id})를 정의합니다. - 명명 규칙 정하기
복수형, 케밥 케이스, trailing slash 사용 여부 등을 문서로 명확히 적어 두고, 새로운 API를 만들 때 이 규칙을 반드시 따르도록 팀 합의를 만듭니다. - Path vs Query 기준 정하기
“식별자/관계는 Path, 검색·필터·페이징·정렬은 Query”라는 기본 원칙을 적용해 기존 URI 중 애매한 부분을 리팩터링 계획에 포함시킵니다. - 버전/언어 정책 합의
아직 v1만 있다면/v1을 붙이는 것을 검토하고, 향후 글로벌 진출 가능성이 있다면 언어/지역 처리 방식을 미리 정해 둡니다. - 새 규칙으로 시범 영역부터 적용
전체를 한 번에 바꾸기보다는, 신규 모듈 또는 작은 도메인부터 새로운 URI 규칙을 적용하고, 안정화되면 점진적으로 나머지 영역을 옮겨가는 방식이 안전합니다.
8. 추가로 생각해볼 점
- HATEOAS(링크 기반 탐색) – 응답 안에 다음에 호출할 수 있는 URI 링크를 포함해 두면, 클라이언트가 “URI를 외워서 사용하는” 대신 응답을 따라 API를 탐색할 수 있습니다.
- SEO와 공유 링크 – 게시글·상품과 같이 외부 공유가 잦은 리소스는
/posts/123-what-is-rest-api처럼 사람이 읽기 쉬운 슬러그를 활용하면 좋습니다. - 모니터링과 로그 – URI 설계가 잘 되어 있으면 모니터링 대시보드에서 “어떤 리소스에 요청이 몰리는지”를 보기 쉬워져, 성능 개선 포인트를 찾는 데도 도움이 됩니다.

0 댓글