REST API 5일차 : URI 설계 원칙과 베스트 프랙티스

리소스 경로와 HTTP 메서드가 연결된 구조를 통해 REST API URI 설계 원칙을 시각적으로 표현한 이미지입니다.

REST API 5일차: URI 설계 원칙과 베스트 프랙티스

한눈에 보는 요약

REST API에서 가장 먼저 마주치는 것이 바로 URI입니다. URI는 사용자가 “이 주소만 봐도 어떤 데이터인지 짐작할 수 있어야” 하고, 개발자는 “새 기능을 추가할 때도 같은 규칙으로 쉽게 확장”할 수 있어야 합니다.

이번 5일차에서는 앞선 글(REST API 디자인 기본 패턴)을 한 단계 더 확장해서, URI를 설계할 때 꼭 알아야 할 원칙과 실무에서 자주 쓰는 베스트 프랙티스를 예시 중심, 초보자도 이해하기 쉬운 방식으로 정리합니다.

목차


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. 내 프로젝트에 적용하는 단계별 가이드

  1. 현재 URI 목록 작성하기
    기존 프로젝트라면 포스트맨/문서/코드에서 사용하는 모든 URI를 표로 정리합니다.
    예: 요청 메서드, URI, 의미, 예시 응답 코드 등.
  2. 리소스 단위로 묶어 보기
    화면이나 기능이 아니라 “유저, 주문, 게시글” 같은 리소스 기준으로 그룹화합니다. 그룹별로 기본 컬렉션 URI(/users, /orders)와 개별 리소스 URI(/users/{id})를 정의합니다.
  3. 명명 규칙 정하기
    복수형, 케밥 케이스, trailing slash 사용 여부 등을 문서로 명확히 적어 두고, 새로운 API를 만들 때 이 규칙을 반드시 따르도록 팀 합의를 만듭니다.
  4. Path vs Query 기준 정하기
    “식별자/관계는 Path, 검색·필터·페이징·정렬은 Query”라는 기본 원칙을 적용해 기존 URI 중 애매한 부분을 리팩터링 계획에 포함시킵니다.
  5. 버전/언어 정책 합의
    아직 v1만 있다면 /v1을 붙이는 것을 검토하고, 향후 글로벌 진출 가능성이 있다면 언어/지역 처리 방식을 미리 정해 둡니다.
  6. 새 규칙으로 시범 영역부터 적용
    전체를 한 번에 바꾸기보다는, 신규 모듈 또는 작은 도메인부터 새로운 URI 규칙을 적용하고, 안정화되면 점진적으로 나머지 영역을 옮겨가는 방식이 안전합니다.

8. 추가로 생각해볼 점

  • HATEOAS(링크 기반 탐색) – 응답 안에 다음에 호출할 수 있는 URI 링크를 포함해 두면, 클라이언트가 “URI를 외워서 사용하는” 대신 응답을 따라 API를 탐색할 수 있습니다.
  • SEO와 공유 링크 – 게시글·상품과 같이 외부 공유가 잦은 리소스는 /posts/123-what-is-rest-api처럼 사람이 읽기 쉬운 슬러그를 활용하면 좋습니다.
  • 모니터링과 로그 – URI 설계가 잘 되어 있으면 모니터링 대시보드에서 “어떤 리소스에 요청이 몰리는지”를 보기 쉬워져, 성능 개선 포인트를 찾는 데도 도움이 됩니다.

Reactions

댓글 쓰기

0 댓글