REST API 설계 원칙

Noah-wilson·2025년 9월 14일

백엔드

목록 보기
1/3

REST API 설계 원칙의 필요성을 느낀 계기

spring & react를 사용해 MVP(Minimum viable Product) 프로젝트를 만드는 과정중 REST API END POINT를 리팩토링 하는 과정에서 처음 rest pai 설계 원칙을 참고하여 END POINT를 개발한 후 end point를 수정해야 했었다.


초기에는 빠르게 프로토 타입으로 프로젝트를 완성하는 것을 목표로 했기 때문에, REST API 설계 원칙을 완전히 이해하지 못한 채로 위 사진과 같이 엉성한 REST API를 구성하였었다.

그러나 리팩토링 시점에 이르러, API 버전 관리, 언더바(_) 사용, 명사의 단수/복수 표현, 리소스-식별자-서브 리소스 계층 구조를 제대로 지키지 않았던 문제들이
생겨났다.

그 결과, 단순히 End Point만 수정하는 과정임에도 불구하고 "이 API가 어떤 요청을 처리하는지"를 코드까지 열어봐야 이해할 수 있었습니다. 프론트엔드 개발 과정에서도, 특정 화면에 필요한 여러 개의 API 중 어떤 것이 어떤 데이터를 반환하는지 직관적으로 구분하기 어려웠습니다.

이를 통해 REST API 설계 원칙을 정확히 지키는 것의 중요성을 크게 깨닫게 되었고, 블로그를 통해 REST API 설계 원칙을 정리하고, 실제 리팩토링 과정을 기록해 두기로 했습니다.

REST API 원칙 및 제약

  1. 일관된 인터페이스(Uniform Interface)
    URI로 지정한 리소스에 대한 조작을 통일하고 한정적인 인터페이스로 수행하여야 한다. 즉, API 설계자가 정한 HTTP METHOD와 규약으로만 수행할 수 있다.

  2. 클라이언트-서버(Client — Server)
    자원를 제공하는 쪽이 서버, 자원을 요청 하는 쪽을 클라이언트라 한다. 이 둘은 독립적으로 분리되어 구성되어야 한다. 각각 별도로 개발되고 대체 가능하도록 유지 해야 한다.

  3. 무상태(Stateless)
    작업에 대한 요청의 상태 정보를 저장하거나 관리 하지 않는다. 세션, 인증, 쿠키 등의 정보는 별도로 저장하기 때문에 요청에 대한 어떠한 상태 정보를 저장하지 않고 단순 처리할 수 있어야 한다. 이를 위해 모든 요청에는 필요한 모든 정보가 포함되어야 한다.

  4. 캐시 가능(Cacheable)
    서버는 캐시 가능 여부를 제공해야 한다. 캐시가 가능 할 경우 클라이언트는 응답 데이터를 재 사용 하여 성능과 서버의 가용성을 올릴 수 있다.

5.계층화된 시스템(Layered System)
계층화된 구조로 구성되어야 한다. 각 계층 자신이 통신하는 컴포넌트 외 다른 계층에 대한 정보를 얻을 수 없도록 분리되어야 한다.

REST API 설계 가이드 라인

  • 명사형 리소스 사용하지 않는다.
    REST API NAMING을 위해서는 동사보다는 명사를 사용하는 것이 좋다.
    REST는 명사를 사용하여 리소스를 표현하고, HTTP METHOD를 이용하여 API의 동작을 표현하여 동사 역할을 하기 때문이다.
- BAD CASE
<https://api.example.com/members/getCart
- GOOD CASE
GET <https://api.example.com/members/cart>
  • 복수형 명사를 사용한다.
    REST API의 RESOURCE는 자원이라고 볼 수 있다.
    RESOURCE는 단일 자원이 아닌 여러개의 자원(컬렉션)을 의미하므로
    복수형을 사용하는 것이 자연스럽다.
    그러므로 식별자를 추가로 사용하여 단일 자원을 얻을 수 있다.
- PLURAL CASE
GET https://api.example.com/orders
- SINGULAR CASE
GET https://api.example.com/orders/:orderId

팀에 따라 단일 자원을 반환 RESOURCE는 단수형 명사를 사용하기도 한다.

  • 언버바(_) 사용하지 말고 하이픈(-)을 사용하라.
    URI를 쉽게 읽고 해석하기 위해서 하이픈(-)을 사용해 가독성을 높일 수 있다.
- BAD CASE
GET <https://api.example.com/productOptions/:optionId>
- GOOD CASE
GET <https://api.example.com/product-options/:optionId>
  • 소문자를 사용하라.
    URL에서는 프로토콜과 호스트 주소는 대소문자를 구분하지 않지만 운영체제에 따라 구분하는 경우가 있으므로 소문자로 통일해서 사용하는것이 관례이다.
- BAD CASE
<https://api.example.com/products/1/Reviews>
- GOOD CASE
<https://api.example.com/products1/reviews>
  • 리소스에 대한 식별이 필요한 경우에 PATH PARAMETER(/{ID})방식을 사용한다.
- BAD CASE
GET <https://api.example.com/orders/orderId?order-id=1>
- GOOD CASE
GET <https://api.example.com/orders/1>
  • 컬렉션 필터링이 필요한 경우 쿼리 파라미터(query parameter)를 사용한다.(예시: 페이지네이션, 정렬 등)
- BAD CASE
GET <https://api.example.com/products/page/2/size/20>
- GOOD CASE
GET <https://api.example.com/products?page=2&size=20>

REST API 버전 관리 방법

URI versioning

https://api.example.com/v1/members/1

Request Parameter versioning

https://api.example.com/members/1?version=1

MIME type versioning

https://api.example.com/members/1
Accept: application/vnd.example.v1+json

Header versioning

https://api.example.com/members/1

- http header
API-Version: 1

가장 많이 사용되는 방식은 URI versioning이다.

요청/응답 규약 전략

Content-Type / Accept 헤더

요청(Request) 시에는 Content-Type으로 본문(body)의 형식을 명시한다.
응답(Response) 시에는 Accept 헤더를 참고하여 클라이언트가 원하는 형식(JSON, XML 등)을 반환한다.
일반적으로 REST API에서는 application/json을 기본으로 사용한다.

- 요청 예시
POST /api/v1/members HTTP/1.1
Content-Type: application/json
Accept: application/json

응답 래핑 (Response Wrapping)

가능하다면 리소스 자체를 그대로 반환한다.

불필요하게 data, result 같은 추가 래퍼(wrapper) 객체로 감싸지 않는다.

- BAD CASE
{
  "data": {
    "id": 1,
    "name": "Alice"
  }
}

- GOOD CASE
{
  "id": 1,
  "name": "Alice"
}

빈 컬렉션 처리

빈 배열은 null이 아닌 빈 배열([]) 로 반환한다.

이 방식이 클라이언트 단에서 처리하기 가장 일관적이다.

- BAD CASE
NULL
- GOOD CASE
[], {}

없는 리소스 처리

존재하지 않는 단일 리소스는 404 Not Found 상태코드로 반환한다.
빈 응답 대신 명확한 상태코드를 사용해야 한다.

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "errorCode": "MEMBER_NOT_FOUND",
  "message": "해당 회원을 찾을 수 없습니다."
}

고민

REST 중첩 리소스를 어떻게 해결해야 하는가?

소규모 프로젝트에서는 리소스 중첩의 Depth가 얕아 크게 문제가 되지 않는다. 하지만 프로젝트 규모가 커질수록 중첩 구조가 점점 깊어질 수 있고, 그럴 경우 URL의 가독성과 유지보수성이 급격히 떨어진다.
이를 해결하기 위해 보통 중첩 구조(Nested Resource) 를 평탄화(Flat Resource) 방식으로 풀어내는 전략을 사용한다.
예시:

2중첩

  • nested resource
# 스토어의 상품 목록
GET /stores/10/products

# 상품의 리뷰 목록
GET /products/200/reviews

# 주문의 아이템 목록
GET /orders/555/items
  • flat resource
# 스토어의 상품 목록
GET /products?storeId=10

# 상품의 리뷰 목록
GET /reviews?productId=200

# 주문의 아이템 목록
GET /order-items?orderId=555

3중첩

  • nested resource
# 스토어의 특정 카테고리 상품 목록
GET /stores/10/categories/3/products

# 고객의 특정 주문의 아이템 목록
GET /customers/99/orders/555/items

# 상품의 특정 옵션(variant)의 재고 이력
GET /products/200/variants/12/inventories

  • flat resource
# 스토어의 특정 카테고리 상품 목록
GET /products?storeId=10&categoryId=3

# 고객의 특정 주문의 아이템 목록
GET /order-items?customerId=99&orderId=555

# 상품의 특정 옵션(variant)의 재고 이력
GET /inventories?productId=200&variantId=12

4중첩

  • nested resource
# 스토어 → 카테고리 → 상품 → 리뷰
GET /stores/10/categories/3/products/200/reviews

# 고객 → 주문 → 아이템 → 반품
GET /customers/99/orders/555/items/7/returns

# 스토어 → 상품 → 옵션(variant) → 가격 변경 이력
GET /stores/10/products/200/variants/12/price-changes
  • flat resource
# 스토어 → 카테고리 → 상품 → 리뷰
GET /reviews?storeId=10&categoryId=3&productId=200

# 고객 → 주문 → 아이템 → 반품
GET /returns?customerId=99&orderId=555&itemId=7

# 스토어 → 상품 → 옵션(variant) → 가격 변경 이력
GET /price-changes?storeId=10&productId=200&variantId=12

직접 예시를 만들어 보면, 3중첩부터는 평탄화(flat) 하는 편이 가독성과 재사용성 측면에서 유리해 보인다.
2중첩의 경우도 상황에 따라 flat 대안이 좋을 수 있지만, 예를 들어 GET /products/200/reviews처럼 목록을 “어느 문맥에서 보느냐”가 중요한 경우에는 2중첩을 유지하는 것이 더 자연스러울 때가 있다.

결국 Path Param은 리소스 식별, Query Param은 필터/정렬/페이지네이션, Body는 리소스 표현(생성/수정 데이터) 이라는 원칙 아래 상황에 맞게 유연하게 선택하는 것이 바람직하다. 단일 리소스 조회/수정/삭제는 가능하면 flat으로 두고, 목록·생성 컨텍스트는 2중첩까지는 허용, 3중첩 이상은 flat로 풀기를 기본 가이드로 삼는것이 바람직해 보인다.

0개의 댓글