본문 바로가기

etc.

[RestAPI] RESTful 웹 API 디자인

웹 API 디자인 모범 사례 - Azure Architecture Center | Microsoft Learn

 

웹 API 디자인 모범 사례 - Azure Architecture Center

플랫폼 독립성과 서비스 진화를 지원하는 웹 API 설계를 위한 모범 사례를 알아봅니다.

learn.microsoft.com

잘 디자인된 웹 API 의 특성

플랫폼 독립성

모든 클라이언트는 API의 내부 구현에 관계없이 API를 호출할 수 있어야 한다. 표준 프로토콜을 사용하고, 클라이언트나 웹 서비스가 교환할 데이터 형식에 대한 메커니즘이 있어야 한다.

 

서비스 진화

Web API는 클라이언트 애플리케이션과 독립적으로 기능을 추가할 수 있어야 한다. API가 진화해도 기존 클라이언트 애플리케이션은 수정 없이 계속 작동할 수 있어야 한다. 클라이언트 애플리케이션이 API의 모든 기능을 완전히 이용할 수 있도록 검색이 가능해야 한다.

REST(Representational State Transfer)

웹 서비스를 디자인하는 아키텍처 접근 방식

 

기본적으로 REST는 어떤 프로토콜과도 독립적이며 HTTP에 연결될 필요가 없다. 그러나 대부분의 REST API 구현은 HTTP를 애플리케이션 프로토콜로 사용하므로 이 가이드에서는 HTTP용 REST API 설계에 중점을 둔다.

 

REST가 HTTP보다 우수한 주요 장점은 개방형 표준을 사용하여 API 또는 클라이언트 애플리케이션의 구현이 특정 구현에 바인딩되지 않는다는 것이다. 예를 들어 REST 웹 서비스는 ASP.NET으로 작성할 수 있으며, 클라이언트 애플리케이션은 HTTP 요청을 생성하고 HTTP 응답을 구문 분석할 수 있는 모든 언어 또는 도구 집합을 사용할 수 있다.

HTTP를 사용하는 RESTful API의 기본 디자인 원칙

  • REST API는 리소스 중심으로 디자인되며, 리소스는 클라이언트에서 액세스할 수 있는 모든 종류의 개체, 데이터, 서비스 등을 포함한다.
  • 리소스마다 해당 리소스를 고유하게 식별하는 URI 식별자가 있다. ex) 특정 고객의 주문 URI: https://adventure-works.com/orders/1
  • 클라이언트는 리소스의 표현 교환으로 서비스와 상호 작용한다. 교환 형식으로는 많은 Web API가 JSON을 사용한다. ex) GET {"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
  • REST API는 균일한 인터페이스를 사용하므로 클라이언트와 서비스 구현을 분리하는 데 도움이 된다. HTTP를 기반으로 하는 REST API의 경우 리소스에 표준 HTTP 동사로 수행할 작업을 표시한다. ex) GET, POST, PUT, PATCH, DELETE
  • REST API는 상태 비저장(stateless) 요청 모델을 사용한다. HTTP 요청은 독립적이고 임의 순서로 발생할 수 있으므로, 요청 사이에 일시적인 상태 정보를 유지할 수 없다. 정보는 리소스 자체에만 저장되며 각 요청은 자동 작업이어야 한다. 클라이언트와 특정 서버 사이에 선호도를 유지할 필요가 없기 때문에 웹 서비스의 확장성이 우수하다. 
  • REST API는 표현에 포함된 하이퍼미디어 링크에 따라 구동된다. ex) 주문의 JSON 표현 - 주문과 관련된 고객을 가져오거나 업데이트하는 링크 포함
{
    "orderID":3,
    "productID":2,
    "quantity":4,
    "orderValue":16.60,
    "links": [
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
    ]
}

 

Web API에 대한 성숙도 모델 (by Leonard Richardson, 2008)

수준 0: 하나의 URI를 정의하고, 모든 작업은 이 URI에 대한 POST 요청이다.

수준 1: 개별 리소스에 대한 별도의 URI를 생성한다.

수준 2: HTTP 메서드를 사용하여 리소스에 대한 작업을 정의한다.

수준 3: 하이퍼미디어(HATEOAS)를 사용한다.

 

실제 대부분의 Web API는 수준 2에 해당된다.

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

웹 API가 표시하는 비즈니스 엔터티에 집중해야 한다.

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

 

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

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

 

리소스가 단일 데이터 항목을 기반으로 할 필요는 없다.

예를 들어 주문 리소스는 내부적으로 관계형 데이터베이스의 여러 테이블로 구현할 수 있지만 클라이언트에 대해서는 단일 엔터티로 표시된다. 단순히 데이터베이스의 내부 구조를 반영하는 API를 만들지 않는다. REST의 목적은 엔터티 및 해당 엔터티에서 애플리케이션이 수행할 수 있는 작업을 모델링하는 것으로 클라이언트에 내부 구현을 노출하지 않도록 한다.

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

 

https://adventure-works.com/orders

 

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

 

URI에 일관적인 명명 규칙을 적용한다.

일반적으로 컬렉션을 참조하는 URI에 복수 명사를 사용할 수 있다. 컬렉션과 그 항목에 대한 URI를 계층 구조로 구성하는 것이 좋다. ex) '/customers' 고객 컬렉션 경로, '/customers/5' ID가 5인 고객의 경로

많은 Web API 프레임워크는 매개 변수가 있는 URI 경로를 기반으로 요청을 라우팅할 수 있으므로 개발자는 경로 /customers/{id}에 대한 경로를 정의할 수 있다.

 

/customers/5/orders는 고객 5에 대한 모든 주문을 나타낼 수 있지만, /orders/99/customer 같은 URI를 사용하여 주문에서 고객으로의 연결을 표시할 수도 있다. 그러나 너무 많이 확장하면 구현이 복잡해진다. HTTP 응답 메시지의 본문에 연결된 리소스 탐색 링크를 제공하는 방법이 좋다. HATEOAS를 사용하여 관련 리소스 탐색 사용하도록 설정 

 

URI를 비교적 간단하게 유지한다.

애플리케이션이 리소스 참조를 지정한 후에는 이 참조를 사용하여 해당 리소스와 관련된 항목을 찾을 수 있어야 한다.

/customers/1/orders/99/products 과 같은 비교적 복잡한 URI 를 /customers/1/orders 로 바꿔서 고객 1의 모든 주문을 찾은 후 /orders/99/products로 연결하여 주문 상품을 찾을 수 있다.

 

TIP. 리소스 URI를 컬렉션/항목/컬렉션보다 더 복잡하게 설계하지 않는 것이 좋다.

 

웹 요청이 많을수록 웹 서버의 부하가 커진다. 따라서 다수의 작은 리소스를 표시하는 "번잡한" Web API를 피해야 한다. 데이터를 비정규화하고 단일 요청을 통해 관련 정보를 검색할 수 있는 더 큰 리소스로 결합하는 것이 좋다. 이 접근 방식과 클라이언트에 필요 없는 데이터를 가져오는 오버헤드의 균형을 조정해야 한다. 큰 개체를 검색하면 요청의 대기 시간이 증가하고 추가 대역폭 비용이 발생할 수 있다. 

 

Web API와 기본 데이터 원본 사이에 종속성이 발생하지 않도록 한다.

데이터가 관계형 데이터베이스에 저장되는 경우 Web API는 각 테이블을 리소스 컬렉션으로 표시할 필요가 없다. Web API를 데이터베이스의 추상화라고 생각하고 필요하다면 데이터베이스와 Web API 사이에 매핑 계층을 도입한다. 이 방법을 사용하면 클라이언트 애플리케이션이 기본 데이터베이스 스키마의 변경 내용으로부터 독립적으로 된다.

 

웹 API에 의해 구현된 일부 작업을 특정 리소스에 매핑하지 못할 수 있다. HTTP GET 요청을 통해 기능을 호출하고 결과를 HTTP 응답 메시지로 반환하는 리소스가 아닌 시나리오를 처리할 수 있다. 예를 들어 더하기 및 빼기 같은 단순한 계산기 작업을 구현하는 Web API는 이러한 작업을 의사 리소스로 표시하고 쿼리 문자열을 사용하여 필요한 매개 변수를 지정하는 URI를 제공할 수 있다. 예를 들어 URI /add?operand1=99&operand2=1에 대한 GET 요청은 본문에 값 100이 포함된 응답 메시지를 반환한다. 그러나 이러한 형식의 URI는 제한적으로 사용해야 한다.

HTTP 메서드 측면에서 API 작업 정의

GET은 지정된 URI에서 리소스의 표현을 검색한다. 응답 메시지의 본문은 요청된 리소스의 세부 정보를 포함한다.

POST는 지정된 URI에 새 리소스를 만든다. 요청 메시지의 본문은 새 리소스의 세부 정보를 제공한다. POST를 사용하여 실제로 리소스를 만들지 않는 작업을 트리거할 수도 있다.

PUT은 지정된 URI에 리소스를 만들거나 대체한다. 요청 메시지의 본문은 만들거나 업데이트할 리소스를 지정한다.

PATCH는 리소스의 부분 업데이트를 수행한다. 요청 본문은 리소스에 적용할 변경 내용을 지정한다.

DELETE는 지정된 URI의 리소스를 제거한다.

 

특정 요청의 효과는 리소스가 컬렉션인지 아니면 개별 항목인지에 따라 달라진다.

 

리소스 POST GET PUT DELETE
/customers 새 고객 만들기 모든 고객 검색 고객 대량 업데이트 모든 고객 제거
/customers/1 Error 고객 1에 대한 세부 정보 검색 고객 1이 있는 경우 고객 1의 세부 정보 업데이트 고객 1 제거
/customers/1/orders 고객 1에 대한 새 주문 만들기 고객 1에 대한 모든 주문 검색 고객 1의 주문 대량 업데이트 고객 1의 모든 주문 제거

 

POST, PUT, PATCH의 차이점

  • POST 요청은 리소스를 만든다. 서버는 새 리소스에 대한 URI를 할당하고 클라이언트에 해당 URI를 반환한다. REST 모델에서는 컬렉션에 POST 요청을 자주 적용한다. 새 리소스가 컬렉션에 추가된다. POST 요청은 새 리소스를 만들지 않고 기존 리소스에 처리할 데이터를 보내는데 사용할 수도 있다.
  • PUT 요청은 리소스를 만들거나 기존 리소스를 업데이트한다. 클라이언트는 리소스의 URI를 지정하고 요청 본문에는 리소스의 완전한 표현이 포함된다. URI가 중복될 경우 리소스가 대체된다. URI가 중복되지 않고 서버에서 리소스 만들기를 지원하는 경우 새 리소스가 생성된다. PUT 요청은 컬렉션보다는 특정 고객 같은 개별 항목 리소스에 가장 자주 적용된다. 서버에서 PUT을 통한 업데이트를 지원하지만 만들기는 지원하지 않을 수 있다. PUT을 통한 만들기 지원 여부는 리소스가 존재하기 전에 클라이언트가 의미 있는 방법으로 리소스에 URI를 할당할 수 있는지 여부에 따라 결정된다. 할당할 수 없는 경우 POST를 사용하여 리소스를 만들고 PUT 또는 PATCH를 사용하여 업데이트한다.
  • PATCH 요청은 기존 리소스에 부분 업데이트를 수행한다. 클라이언트는 리소스의 URI를 지정한다. 요청 본문은 리소스에 적용할 변경 내용을 지정한다. 클라이언트가 리소스의 전체 표현이 아닌 변경 내용만 보내기 때문에 PUT을 사용하는 것보다 이 방법이 더 효율적일 수 있다. 또한 서버에서 리소스 만들기를 지원하는 경우 기술적으로 PATCH는 새 리소스를 만들 수 있다("null" 리소스에 대한 업데이트 지정).

PUT 요청은 idempotent여야 한다. 클라이언트가 동일한 PUT 요청을 여러 번 제출하는 경우 같은 값을 사용하여 같은 리소스가 수정되므로 그 결과가 항상 같아야 한다. POST, PATCH 요청은 반드시 idempotent가 된다는 보장이 없다.

HTTP 의미 체계 준수

미디어 유형 (MIME 유형)

클라이언트와 서버는 리소스 표현을 교환한다. POST 요청에서는 요청 본문에 만들 리소스의 표현이 포함되고, GET 요청에서는 응답 본문에 가져온 리소스의 표현이 포함된다.

 

HTTP 프로토콜에서 형식은 MIME 유형이라고도 하는 미디어 유형을 사용하여 지정한다. 이진 데이터가 아닌 경우, 대부분 JSON(미디어 유형 = application/json) 및 XML(미디어 유형 = application/xml)을 지원한다.

 

요청 또는 응답의 Content-Type 헤더는 표현 형식을 지정한다. 서버에서 요청 미디어 유형을 지원하지 않으면 HTTP 상태 코드 415(지원되지 않는 미디어 유형)를 반환해야 한다.

ex) JSON 데이터를 포함하는 POST 요청

POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

 

클라이언트 요청에 클라이언트가 응답 메시지에서 서버로부터 받는 미디어 유형 목록을 포함하는 Accept 헤더가 포함될 수 있다. 서버가 나열된 미디어 유형 중 어떤 것도 일치시킬 수 없는 경우 HTTP 상태 코드 406(허용되지 않음)을 반환해야 한다.

GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

GET 메서드

  • 성공적인 GET 메서드: HTTP 상태 코드 200(정상) 반환
  • 리소스를 찾을 수 없는 경우: 404(찾을 수 없음) 반환
  • 요청이 처리되었지만 HTTP 응답에 포함된 응답 본문이 없는 경우: 204(콘텐츠 없음) 반환 ex) 일치 항목이 없는 검색 작업

POST 메서드

  • 새 리소스를 만드는 경우: HTTP 상태 코드 201(만들어짐) 반환. 새 리소스의 URI는 응답의 Location 헤더에 포함. 응답 본문은 리소스의 표현 포함)
  • 일부 처리를 수행하지만 새 리소스를 만들지 않는 경우: 200을 반환하고 작업의 결과를 응답 본문에 포함. 반환할 결과가 없으면 응답 본문 없이 204(내용 없음) 반환
  • 클라이언트가 잘못된 데이터를 요청에 배치: 400(잘못된 요청) 반환. 응답 본문에는 오류에 대한 추가 정보 또는 자세한 정보를 제공하는 URI 링크 포함 가능

PUT 메서드

  • 새 리소스를 만드는 경우: HTTP 상태 코드 201(만들어짐) 반환.
  • 기존 리소스를 업데이트할 경우: 200(정상) 또는 204(내용 없음) 반환
  • 기존 리소스를 업데이트할 수 없는 경우: 409(충돌) 반환을 고려
  • 일괄 HTTP PUT 작업(컬렉션의 복수 리소스에 대한 업데이트 일괄 처리): PUT 요청은 컬렉션의 URI를 지정해야 하며, 요청 본문에 수정할 리소스의 세부 정보를 지정해야 한다. 이 접근 방식은 데이터 전송량을 줄이고 성능을 향상시킬 수 있다.

PATCH 메서드

클라이언트는 PATCH 요청을 통해 업데이트를 패치 문서의 형태로 기존 리소스에 보낸다. 서버는 패치 문서를 처리하여 업데이트를 수행한다. 패치 문서는 변경 내용만을 설명한다. PATCH 메서드 스펙(RFC 5789)은 패치 문서에 대한 특정 형식을 정의하지 않기 때문에 형식은 요청의 미디어 형식에서 유추해야 한다.

 

JSON 기반 주요 패치 형식: JSON 패치, JSON 병합 패치

 

JSON 병합 패치 (RFC 7396)

비교적 간단한 방법

  • 패치 문서는 원래 JSON 리소스와 동일한 구조를 갖지만 변경 또는 추가할 필드의 하위 집합만을 포함한다.
  • 패치 문서에서 필드 값에 null을 지정하여 필드를 삭제할 수 있다. 원래 리소스가 명시적 null 값을 가질 수 있으면 병합 패치가 적합하지 않다.
  • 패치 문서는 서버에서 업데이트 적용 순서를 지정하지 않는다. 데이터 및 도메인에 따라 이 점이 중요할 수도 있다.
  • 미디어 유형: application/merge-patch+json
// 원래 리소스 (Json 형식)
{
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

/** 패치 문서: 변경 또는 추가할 필드만을 포함
 * price 업데이트, color 삭제, size 추가 (name, category 수정X)
 */
{
    "price":12,
    "color":null,
    "size":"small"
}

 

JSON 패치 (RFC 6902)

  • 작업의 결과로 적용할 변경 내용을 지정한다.
  • 작업에는 추가, 제거, 바꾸기, 복사 및 테스트(값의 유효성 검사)등이 있다.
  • 미디어 유형: application/json-patch+json

PATCH 요청을 처리할 때 발생할 수 있는 오류 조건과 적절한 HTTP 상태 코드

  • 지원되지 않는 패치 문서 형식: 415(지원되지 않는 미디어 형식)
  • 잘못된 패치 문서 형식: 400(잘못된 요청)
  • 패치 문서가 유효하지만 현재 상태에서는 변경 내용을 리소스에 적용할 수 없음: 409(충돌)

DELETE 메서드

  • 삭제 작업 성공: 프로세스가 성공적으로 처리되었지만 응답 본문에 추가 정보가 없음을 나타내는 HTTP 상태 코드 204(콘텐츠 없음) 반환
  • 리소스가 없는 경우: 404(찾을 수 없음) 반환

비동기 작업

클라이언트가 요청한 작업을 완료하는 데 시간이 걸릴 수 있다. 처리 작업이 완료될 때까지 기다렸다가 클라이언트에 응답을 보내는 경우 허용되지 않는 수준의 대기 시간이 발생할 수 있기 때문에 비동기 작업을 고려할 수 있다. 이 경우 요청 처리가 수락되었지만 아직 완료되지 않았음을 나타내는 HTTP 상태 코드 202(수락됨)를 반환한다.

 

클라이언트가 처리를 모니터링할 수 있도록 비동기 요청의 상태를 반환하는 엔드포인트를 표시해야 한다. 202 응답의 Location 헤더에 상태 엔드포인트의 URI를 포함한다.

HTTP/1.1 202 Accepted
Location: /api/status/12345

 

클라이언트가 엔드포인트에 GET 요청을 보내는 경우 응답에 요청의 현재 상태가 포함되어야 한다. 필요에 따라 예상 완료 시간 또는 작업 취소 링크를 포함할 수 있다.

HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}

 

비동기 작업에서 새 리소스를 만드는 경우 작업 완료 후 상태 엔드포인트에서 상태 코드 303(다른 항목 보기)을 반환한다. 303 응답에 새 리소스의 URI를 제공하는 Location 헤더를 포함한다.

HTTP/1.1 303 See Other
Location: /api/orders/12345

 

자세한 내용은

장기 실행 요청에 대한 비동기 지원 제공 및 비동기 Request-Reply 패턴 참조

메시지 본문의 빈 집합

성공적인 응답에서 본문이 비어 있다면 상태 코드는 200(OK)이 아닌 204(콘텐츠 없음)여야 한다.

ex) 항목이 없는 필터링 요청에 대한 응답 (빈 집합)

데이터 필터링 및 페이지 매기기

단일 URI를 통해 리소스 컬렉션을 표시하면 정보의 하위 집합(부분)만 필요할 때에도 애플리케이션이 대량의 데이터를 가져올 수 있다.

예를 들어 클라이언트 애플리케이션에서 비용이 특정 값을 초과하는 주문을 검색할 때, /orders URI에서 모든 주문을 요청한 후 클라이언트 쪽에서 필터링할 것이다. 이 프로세스는 매우 비효율적이다. 

 

컬렉션 리소스에 대한 GET 요청은 다수의 항목을 반환할 가능성이 있다. 단일 요청에서 반환하는 데이터의 양이 제한되도록 Web API를 디자인해야 한다. 검색할 최대 항목 수(limit)컬렉션의 시작 오프셋(offset)을 지정하는 쿼리 문자열을 지원해보자.

 

/orders?limit=25&offset=50

 

서비스 거부 공격을 방지하기 위해 반환되는 항목 수를 제한하는 방안도 고려해 보자. 클라이언트 애플리케이션을 돕기 위해, 페이지가 매겨진 데이터를 반환하는 GET 요청은 컬렉션의 사용할 수 있는 총 리소스 수를 나타내는 모종의 메타데이터 형식을 포함해야 한다.

정렬 매개 변수를 제공하여 필드 이름을 /orders?sort=ProductID 같은 값으로 데이터를 가져올 때 데이터를 정렬하는 전략을 사용할 수 있다. 그러나 쿼리 문자열 매개 변수는 여러 캐시 구현에서 키로 사용되는 리소스 식별자의 일부를 구성하기 때문에 이 접근 방식은 캐싱에 나쁜 영향을 미칠 수 있다.

각 항목에 대량의 데이터가 포함된 경우 각 항목에 대해 반환되는 필드를 제한하도록 이 접근 방식을 확장할 수 있다. 예를 들어 쉼표로 필드를 구분하는 /orders?fields=ProductID,Quantity 같은 쿼리 문자열 매개 변수를 사용할 수 있다.

 

쿼리 문자열의 모든 선택적 매개 변수에 의미 있는 기본값을 제공하자. 예를 들어 페이지 매김을 구현하는 경우 limit 매개 변수를 10으로, offset 매개 변수를 0으로 설정하고, 주문을 구현하는 경우 정렬 매개 변수를 리소스의 키로 설정하고, 프로젝션을 지원하는 경우 fields 매개 변수를 리소스의 모든 필드로 설정한다.

'etc.' 카테고리의 다른 글

Spring Boot Security  (0) 2023.03.29
[Intellij] 서블릿 프로젝트 생성  (0) 2023.01.30