인터넷 기본 지식

#9. RESTful 웹 API 디자인

흰제비갈매기 2024. 6. 19. 14:45

대부분의 웹 어플리케이션은 client가 application과 상호 작용하는 데 사용할 수 있는 API를 표시한다. 잘 디자인된 API는 아래와 같은 특성을 지원해야 한다.

 

  1. 플랫폼 독립성: 모든 client는 내부의 API가 어떻게 구현되는지 몰라도 API를 호출할 수 있어야 한다. 그러기 위해서는 표준 프로토콜을 사용해야 하고, client 및 web service가 교환할 데이터 형식에 동의할 수 있는 메커니즘이 있어야 한다.
  2. 서비스 진화: Web API는 client 어플리케이션과 독립적으로 기능을 진화시키고 추가할 수 있어야 한다. API가 진화해도 기존 클라이언트 어플리케이션은 수정 없이 계속 작동할 수 있어야 한다. 모든 기능은 client application이 해당 기능을 완전히 이용할 수 있도록 검색이 가능해야 한다.

 

REST는 무엇인가?

2000년에 Roy Fielding이 제안했으며 웹 서비스 디자인을 위한 아키텍처 접근 방식이다. 다음과 같은 특징을 지니고 있다.

  • 하이퍼미디어 기반 분산 시스템을 구축하기 위한 아키텍처 스타일
    • 리소스를 URI로 식별하고. HTTP 메서드(POST, GET, PUT, DELETE)를 사용하여 상태를 전이
  • 기본 프로토콜과 독립적이며, HTTP에 종속되지 않는다.
    • REST는 특정 통신 프로토콜(e.g. HTTP, FTP, SMTP 등)에 의존하지 않는 아키텍처 스타일이다.
    • REST는 HTTP 프로토콜에만 제한되지 않는다. 허나 HTTP는 REST의 가장 일반적인 구현 방법이지만, 필수적인 요소는 아니다.
  • 대부분의 REST API 구현은 HTTP를 사용

HTTP와 REST의 차이점으로, REST는 개방형 표준을 사용하며, 특정 구현에 바인딩 되지 않는다.

  • 개방형 표준은 누구나 접근하고 사용할 수 있는 표준을 의미한다. 따라서 REST API는 다양한 클라이언트에서 호환성을 가질 수 있다.
  • REST는 특정 서버 기술이나 클라이언트 기술에 종속되지 않는다. 즉, RESTful 웹 서비스는 다양한 프로그래밍 언어와 프레임워크로 구현될 수 있다.

 

REST 웹 서비스는 ASP.NET 등 다양한 언어로 작성 가능
클라이언트 어플리케이션은 HTTP 요청 생성 및 응답 구문 분석을 위해 어떤 언어 또는 도구 집합도 사용 가능

 

다음은 HTTP를 사용하는 RESTful API의 몇 가지 기본 디자인 원칙이다.

  • REST API는 리소스를 중심으로 디자인된다. 클라이언트에서 액세스할 수 있는 모든 종류의 개체, 데이터 또는 서비스가 리소스에 포함된다.
  • 리소스마다 해당 리소스를 고유하게 식별하는 URI인 식별자가 있다. 예를 들어 특정 고객 주문 문의의 URI는 다음과 같다.
https://adventure-works.com/orders/1
  • client가 리소스의 표현을 교환하여 서비스와 상호작용한다. 많은 Web API가 교환 형식으로 JSON을 사용한다. 예를 들어 위에 나열된 URI에 대한 GET 요청은 이 응답 본문을 반환할 수 있다.
{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
  • 균일한 인터페이스
    • REST API는 균일한 인터페이스를 사용하여 클라이언트와 서버 간의 상호작용을 표준화한다.
    • HTTP를 기반으로 하는 REST API에서는 GET, POST, PUT, PATCH, DELETE와 같은 표준 HTTP 동사를 사용하여 리소스에 작업을 수행한다.
      GET /users/1       -> 사용자 ID 1에 대한 정보 가져오기
      POST /users        -> 새로운 사용자 생성
      PUT /users/1       -> 사용자 ID 1의 전체 정보 업데이트
      PATCH /users/1     -> 사용자 ID 1의 일부 정보 업데이트
      DELETE /users/1    -> 사용자 ID 1 삭제
  • 상태 비저장 요청 모델
    • REST API는 상태 비저장(stateless) 요청 모델을 사용한다.
    • 각 HTTP 요청은 독립적이어야 하며, 요청 간에 일시적인 상태 정보를 유지하지 않는다.
    • 이는 모든 요청이 독립적으로 작동해야 하며, 서버는 클라이언트의 상태를 기억하지 않는다는 것을 의미한다.
    • # 첫 번째 요청 GET /orders/123 # 서버는 이 요청을 처리하며, 클라이언트의 이전 상태를 기억하지 않습니다. # 두 번째 요청 POST /orders # 서버는 이 요청도 독립적으로 처리합니다.
  • 확장성
    • 상태 비저장 모델 덕분에 REST API는 확장성이 뛰어난다.
    • 클라이언트와 특정 서버 간에 선호도를 유지할 필요가 없으므로, 모든 서버가 모든 클라이언트의 요청을 처리할 수 있다.
    • 그러나 백엔드 데이터 저장소의 쓰기 작업 등 다른 요소가 확장성을 제한할 수 있다.
  • 데이터 저장소의 확장 전략
    • 데이터 저장소를 확장하는 전략에는 가로, 세로, 및 기능 데이터 분할이 포함된다.
    • 이러한 전략을 통해 데이터 저장소의 확장성을 높일 수 있다.
    • 1. 가로 분할(Sharding): 데이터를 여러 서버에 분산 저장하여 읽기 및 쓰기 작업을 분산시킨다. 2. 세로 분할(Vertical Partitioning): 데이터 베이스의 테이블을 기능별로 분할하여 각 기능을 독립적으로 확장한다. 3. 기능 데이터 분할(Functional Partitioning): 애플리케이션의 기능별로 데이터를 분할하여 관리한다.
  • REST API는 표현에 포함된 하이퍼미디어 링크에 따라 구동된다. 예를 들어 다음은 order의 JSON 표현을 보여준다. order과 관련된 고객을 가져오거나 업데이트하는 링크를 포함하고 있다.
{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links": [
    // rel: 현재 리소스와 관련된 다른 리소스에 대한 링크 파일
    //        링크된 리소스와의 관계를 설명한다. 여기서는 product로 제품과 관련된 리소스임을 나타낸다.
    // href: 링크된 리소스의 URI이다. 
    // action: HTTP 메서드를 지정한다.
    {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
    {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
  ]
}

 

Leonard Richardson은 Web API의 성숙도를 측정하기 위해 4단계 모델을 제안했다. 이 모델은 API의 RESTful 성격을 평가하는 데 사용된다.

  1. 수준 0: 단일 URI
    • 특징: 모든 작업이 단일 URI에 대한 POST 요청으로 이루어진다.
    • 예시: 모든 요청이 http://example.com/api 에 POST로 전송되어야 하는 경우
      POST /api
      {
      "action": "getUser",
      "userId": 1
      }
  2. 수준 1: 리소스별 URI
    • 특징: 개별 리소스에 대해 별도의 URI를 정의
    • 예시: 사용자. 제품 등 각각의 리소스에 대해 별도의 URI를 사용
      GET /users/1
      GET /products/2
  3. 수준 2: HTTP 메서드
    • 특징: HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 리소스에 대한 작업을 정의한다.
    • 예시: 각 URI에 대해 적절한 HTTP 메서드를 사용하여 작업을 수행한다.
      GET /users/1       // 사용자 정보 가져오기
      POST /users        // 새 사용자 생성
      PUT /users/1       // 사용자 정보 업데이트
      DELETE /users/1    // 사용자 삭제
  4. 수준 3: 하이퍼미디어(HATEOAS)
    • 특징: 응답에 하이퍼미디어 링크를 포함하여 클라이언트가 다음 작업을 탐색할 수 있게 한다.
    • 예시: 리소스에 대한 작업 링크를 응답에 포함한다.
      {
      "userId": 1,
      "name": "John Doe",
      "links": [
        {"rel": "self", "href": "/users/1"},
        {"rel": "orders", "href": "/users/1/orders"}
       ]
      }



리소스를 중심으로 API 디자인 구성

즉, 웹 API가 표시하는 entity에 집중해야 한다. 예를 들어 전자 상거래 시스템에서 기본 entity는 customer과 order이다.
주문 정보가 포함된 HTTP POST 요청을 전송하여 주문 만들기를 구현할 수 있다. HTTP 응답은 order이 성공적으로 수행되었는지 여부를 나타낸다. 가능하다면 리소스 URI는 동사(리소스에 대한 작업)가 아닌 명사(리소스)를 기반으로 해야 한다

 

https://adventure-works.com/orders // Good

https://adventure-works.com/create-order // Avoid

 

리소스가 단일 실제 데이터 항목을 기반으로 할 필요는 없다. 예를 들어 order 리소스는 내부적으로는 관계형 DB의 여러 table로 구현할 수 있지만, 클라이언트에 대해서는 단일 entity로 표시된다. 단순히 DB의 내부 구조를 반영하는 API를 만들면안된다. REST의 목적은 entity 및 해당 entity에서 애플리케이션이 수행할 수 있는 작업을 모델링하는 것이다. 클라이언트는 내부 구현에 노출되면 안된다.

// 잘못된 설계
GET /orders_table/123
GET /customers_table/456
GET /products_table/789

// 올바른 설계
GET /orders/123

위와 endpoint를 통해 필요한 모든 정보를 얻을 수 있다.

{
  "orderId": 123,
  "orderDate": "2023-06-01",
  "customer": {
    "customerId": 456,
    "name": "John Doe",
    "email": "john.doe@example.com"
  },
  "products": [
    {
      "productId": 789,
      "productName": "Widget",
      "quantity": 2,
      "price": 19.99
    }
  ],
  "total": 39.98
}

 

entitiy는 종종 collection(주문, 고객)으로 그룹화된다. collection은 collection 내 항목과는 별도의 리소스이며 고유한 URI가 있어야 한다.
예를 들어 다음 URI는 주문 컬랙션을 나타낼 수 있다.

 

https://adventure-works.com/orders

 

collection URI에 HTTP GET 요청을 보내면 collection에 있는 항목 목록을 검색한다. 또한 collection의 항목마다 고유의 URI가 있다.
항목의 URI에 대한 HTTP GET 요청은 해당 항목의 세부 정보를 반환한다.

 

URI에 일관적인 명명 규칙을 적용한다. 일반적으로 이렇게 하면 collection을 참조하는 URI에 대해 복수 명사를 사용할 수 있다.
collection 및 항모에 대한 URI를 계층 구조로 구성하는 것이 좋다. 예를 들어 /customers는 고객 collection의 경로이고, /customers/5는 ID가 5인 고객의 경로이다. 이 접근 방식을 사용하면 웹 API를 직관적으로 유지할 수 있다. 또한 많은 Web API 프레임워크는 매개 변수가 있는 URI 경로를 기반으로 요청을 routing할 수 있으므로 개발자는 경로 /customers/{id} 에 대한 경로를 정의할 수 있다.

 

서로 다른 리소스 형식과 이러한 연결을 표시하는 방법 사이의 관계도 고려해야 한다. 예를 들어 /customers/5/orders는 고객 5에 대한 모든 주문을 나타낼 수 있다. 반대 방향으로 이동하여 /orders/99/customer 같은 URI를 사용하여 주문에서 고객으로의 연결을 표시할 수도 있다.
그러나 이 모델을 너무 확장하면 구현이 어려울 수 있다. HTTP 응답 메시지의 본문에 연결된 리소스에 대한 탐색 가능한 링크를 제공하는 방법이 좋다.

 

좀 더 복잡한 시스템에서는 /customers/1/orders/99/products 처럼 클라이언트가 여러 관계 수준을 탐색할 수 있는 URI를 제공하고 싶을 수 있다. 그러나 이 수준의 복잡성은 유지하기 어려울 수 있으며 나중에 리소스 사이의 관계가 변하면 유연성이 떨어진다. 그 대신 URI를 비교적 간단하게 유지해보자. 애플리케이션이 리소스 참조를 지정한 후에는 이 참조를 사용하여 해당 리소스와 관련된 항목을 찾을 수 있어야 한다. 이전 쿼리를 /customers/1/orders URI로 바꿔서 고객 1의 모든 주문을 찾은 후 /orders/99/products로 바꿔서 이 주문의 제품을 찾을 수 있다.

 

팁!
리소스 URI를 컬렉션/항목/컬렉션 보다 더 복잡하게 요구하지 않는 것이 좋다.