REST란?
REST는 REpresentational State Transfer의 약자이며 분산 하이퍼미디어 시스템을 위한 아키텍처 스타일이다. 2000년에 로이 필딩(Roy Fielding)이 논문에서 처음 제시했다. 다른 아키텍처 스타일과 마찬가지로 REST에는 기본 원칙과 제약 조건이 있고 서비스 인터페이스를 RESTful로 참조하는 경우 이러한 원칙을 충족해야한다.
REST의 기본 원칙
1. 통일된 인터페이스
구성 요소 인터페이스에 일반성의 원칙을 적용하여 전체 시스템 아키텍처를 단순화하고 상호 작용이 어떻게 이루어지는지 잘 볼 수 있도록 가시성을 향상시킬 수 있다. 다음 네 가지 제약 조건은 균일한 REST 인터페이스를 달성할 수 있다.
- Identification of resources (리소스 식별) : 인터페이스는 클라이언트와 서버 간의 상호 작용과 관련된 각 리소스를 고유하게 식별해야 한다.
- Manipulation of resources through representations (표현을 통한 리소스 조작) : 리소스는 서버 응답에서 균일한 표현을 가져야 합니다. API 사용자는 이러한 표현을 통해 서버의 리소스 상태를 수정해야 한다.
- Self-descriptive messages (자체 설명 메시지) : 각 리소스 표현은 메시지 처리 방법을 설명하기에 충분한 정보를 전달해야 한다. 또한 클라이언트가 리소스에서 수행할 수 있는 추가 작업에 대한 정보도 제공해야 한다.
- Hypermedia as engine of application state (애플리케이션 상태 엔진으로서의 하이퍼미디어) : 클라이언트는 애플리케이션의 초기 URI만 가져야 한다. 클라이언트 응용 프로그램은 하이퍼링크를 사용하여 다른 모든 리소스와 상호 작용을 동적으로 구동해야 한다.
2. 클라이언트-서버
클라이언트-서버 디자인 패턴은 클라이언트와 서버 구성 요소가 독립적으로 발전하는데 도움이 되는 관심사 분리를 적용한다.
데이터 저장 문제(서버)에서 사용자 인터페이스 문제(클라이언트)를 분리함으로써, 여러 플랫폼에서 사용자 인터페이스의 이식성을 개선하고 서버 구성 요소를 단순화하여 확장성을 향상시킬 수 있다.
- 무상태성
무상태성은 클라이언트에서 서버로 보내는 요청을 이해하고 수행하는 데 필요한 모든 정보를 포함하도록 한다. 서버는 이전에 서버에 저장된 컨텍스트 정보를 이용할 수 없다. 이러한 이유로 클라이언트 애플리케이션은 세션 상태를 유지해야할 필요성이 있다. - 캐시 가능
캐시 가능 제약 조건에서는 서버 응답이 자신을 캐시 가능 또는 캐시 불가로 지정해주어야 한다. 응답을 캐시할 수 있는 경우 클라이언트는 나중에 동등한 요청을 보냈을 때 지정된 기간 동안 응답 데이터를 재사용할 수 있는 권한을 가진다. - 레이어드 시스템
레이어드 시스템을 사용하면 구성 요소 동작을 제한하여 아키텍처를 계층적으로 구성할 수 있다. 계층화된 시스템에서 각 구성 요소는 자신이 상호 작용하는 계층 너머는 볼 수 없다. - Code on Demand (선택사항)
REST는 애플릿(applet)이나 스크립트의 형태로 코드를 다운로드하고 실행하여 클라이언트 기능을 확장할 수 있도록 한다. 다운로드한 코드는 미리 구현해야 하는 기능의 수를 줄여 클라이언트를 단순화한다. 서버는 클라이언트에 전달되는 기능의 일부를 코드 형태로 제공할 수 있으며 클라이언트는 코드를 실행하기만 하면 된다.
RESTful API 설계 원칙
REST API는 클라이언트가 액세스할 수 있는 모든 종류의 개체, 데이터 또는 서비스인 리소스를 중심으로 설계되었다.
리소스에는 해당 리소스를 고유하게 식별하는 URI인 식별자가 있다. 예를 들어 특정 상품 주문에 대한 URI는 다음과 같다.
[https://localhost:8080/products/1](https://localhost:8080/products/1)
클라이언트는 리소스 표현을 통해 서비스와 상호작용한다. 많은 웹 API는 JSON을 교환 형식으로 사용한다. 예를 들어 위에 나열된 URI에 대한 GET 요청은 다음과 같은 응답 본문을 반환할 수 있다.
{"orderId": 1, "name": "치킨", "productId": 1, "price": 22000}
REST API는 클라이언트와 서비스 구현을 분리하는 데 도움이 되는 균일한 인터페이스를 사용한다. HTTP를 기반으로 구축된 REST API의 경우 통일된 인터페이스에는 표준 HTTP 동사를 사용하여 리소스에 대한 작업을 수행하는 것을 포함한다.
REST API는 상태 비저장 요청 모델을 사용한다. HTTP 요청은 독립적이어야 하며 어떤 순서로든 발생할 수 있으므로 요청 간에 일시적인 상태 정보를 유지하는 것은 불가능하다. 정보가 저장되는 유일한 위치는 리소스 자체이며 각 요청은 원자적 작업이어야 한다.
리소스를 중심으로 API 설계 구성
웹 API에서 노출되는 비즈니스 엔티티에 집중해야 한다. 예를 들어 전자상거래 시스템에서 기본적인 엔티티는 고객과 주문일 수 있고 주문 정보가 포함된 HTTP POST 요청을 전송하여 주문을 생성할 수 있다. HTTP 응답은 주문이 성공적으로 이루어졌는지 여부를 나타낸다. 가능한 경우 리소스 URI는 동사(리소스에 대한 작업)가 아닌 명사(리소스)를 기반으로 해야 한다.
https://localhost:8080/order // Good
https://localhost:8080/craete-order // Avoid
리소스는 단일 물리적 데이터 항목을 기반으로 할 필요는 없다. 예를 들어 주문 리소스는 관계형 데이터베이스의 여러 테이블로 내부적으로 구현될 수 있지만 클라이언트에게는 단일 엔티티로 표시될 수 있다. 단순히 데이터베이스의 내부 구조를 그대로 투영하는 API를 작성하는 것은 지양하고 클라이언트가 내부 구현에 노출되도록 해서는 안 된다.
엔티티는 종종 컬렉션(주문, 고객)으로 함께 그룹화된다. 컬렉션은 컬렉션 내의 항목과는 별개의 리소스이며 고유한 URI를 가져야 한다. 예를 들어 다음 예시는 주문 모음을 나타낼 수 있다.
https://localhost:8080/orders
컬렉션 URI에 HTTP GET 요청을 보내면 컬렉션의 항목 목록이 검색된다. 컬렉션의 각 항목에는 고유한 URI도 있고 각 항목의 URI에 대한 HTTP GET 요청은 해당 항목의 세부 정보를 반환한다.
URI에서는 일관된 명명 규칙을 채택해야 한다. 일반적으로 컬렉션을 참조하는 URI에 대해 복수 명사를 사용하는 것이 도움이 된다. 또한 컬렉션 및 항목에 대한 URI를 계층 구조로 구성하는 것이 좋습니다. 예를 들어 /customers
는 고객 컬렉션에 대한 경로이고 /customers/4
는 ID가 5인 고객에 대한 경로이다. 이러한 접근 방식은 웹 API를 직관적으로 유지하는 데 도움이 된다.
또한 서로 다른 유형의 리소스 간의 관계와 이러한 연결을 노출할 수 있는 방법을 고려해야 한다. 예를 들어 /customers/5/orders
는 고객 5의 모든 주문을 나타낼 수 있다. 다른 예시로 /orders/99/customer
로 99번 order를 주문한 고객을 찾을 수도 있다. 하지만 이런 모델을 너무 확장하면 구현하기가 번거로울 수 있다. 더 나은 방법 중 한 가지는 HTTP 응답 메시지의 body에 연결된 리소스에 대한 탐색 가능한 링크를 제공하는 것이다.
더 복잡한 시스템에서 /customers/1/orders/99/products
와 같은 더 복잡한 관계를 가지는 URI에 대한 접근을 시도할 수도 있다. 하지만 이런 수준의 복잡성은 유지하기 어려울 수 있으며 리소스 간의 관계가 미래에 변경될 경우 유연하게 대처하기 어렵다. 애플리케이션에 리소스에 대한 참조가 있으면 이 참조를 사용하여 해당 리소스와 관련된 항목을 찾을 수 있어야 한다. 앞의 쿼리를 URI로 대체하여 /customers/1/orders
로 고객 1의 모든 주문을 찾은 다음 /orders/99/products
를 통해 이 주문에 포함된 제품들을 찾을 수 있다.
HTTP 메서드 측면에서의 API 작업 정의
HTTP 프로토콜은 요청에 의미를 할당하는 여러 메서드를 정의한다 대부분의 RESTful API에서 사용되는 일반적인 HTTP 메서드는 다음과 같다.
- GET은 지정된 URI에서 리소스 표현을 검색한다. 응답 메시지의 body에는 요청된 리소스의 세부 정보가 포함된다.
- POST는 지정된 URI에 새 리소스를 만든다. 요청 메시지의 body는 새 리소스의 세부 정보를 제공한다. POST는 실제로 리소스를 생성하지 않는 작업에 대해서도 사용할 수 있다.
- PUT은 지정된 URI에서 리소스를 만들거나 수정한다. 요청 메시지의 body에는 새로 만들거나 수정할 리소스에 대한 정보를 제공한다.
- PATCH는 리소스의 수정을 수행한다. 요청 메시지의 body는 리소스에 적용할 변경 사항 집합을 정의한다.
- DELETE는 지정된 URI에서 리소스를 제거한다.
- 특정 요청의 효과는 리소스가 컬렉션인지 개별 항목인지에 따라 달라진다 다음 표에는 전자 상거래 예제를 사용하여 대부분의 RESTful 구현에서 채택하는 일반적인 규칙이 요약되어 있다.
리소스 | POST | GET | PUT | DELETE |
---|---|---|---|---|
/customers | 새로운 customer를 만든다 | 모든 customer의 목록을 가져온다 | 다수의 customer를 한 번에 업데이트한다 | 모든 customer를 삭제한다 |
/customers/1 | Error | customer 1에 관한 모든 정보를 얻는다 | customer 1이 존재한다면 정보를 수정한다 | customer 1을 삭제한다 |
/customers/1/orders | customer 1의 새로운 주문을 만든다 | customer 1의 모든 주문 목록을 가져온다 | customer 1의 여러 주문들을 한 번에 업데이트한다 | customer 1의 모든 주문들을 삭제한다 |
HTTP 의미 체계 준수
GET method
성공적인 GET 요청은 일반적으로 HTTP 상태코드 200(OK)를 반환한다. 리소스를 찾을 수 없는 경우 404(Not Found)를 반환해야 한다. 요청이 이행되었지만 HTTP 응답에 포함된 body가 없는 경우 HTTP 상태코드 204(No Content)를 반환해야 한다. 예를 들어 일치하는 항목이 없는 검색 요청은 204를 반환할 수 있다.
POST method
POST 메서드가 새 리소스를 만드는 경우 HTTP 상태 코드 201(Created)을 반환한다. 새 리소스의 URI는 응답의 Location 헤더에 포함된다 .응답 메시지의 body에는 리소스 표현이 포함된다.
메서드가 일부 처리를 수행하지만 새 리소스를 생성하지 않는 경우 메서드는 HTTP 상태 코드 200을 반환하고 작업 결과를 응답 본문에 포함할 수 있다. 또는 반환할 결과가 없는 경우 메서드는 응답 메시지에 body가 없는 채로 HTTP 상태코드 204를 반환할 수 있다.
PUT method
PUT 메서드가 새 리소스를 생성하면 POST 메서드와 마찬가지로 HTTP 상태코드 201(Created)을 반환한다. 메서드가 기존 리소스를 업데이트하는 경우 200(OK) 또는 204(No Content)를 반환한다. 경우에 따라 기존 리소스를 업데이트하지 못할 수도 있다. 이 경우 HTTP 상태 코드 409(Conflict) 반환을 고려할 수 있다.
컬렉션의 여러 리소스에 대한 업데이트를 일괄 처리할 수 있는 대량 HTTP PUT 작업의 구현을 고려할 수도 있다. PUT 요청은 컬렉션의 URI를 지정해야 하며 요청 메시지의 body에는 수정할 리소스의 세부 정보를 지정해야 한다. 이러한 접근 방식은 성능을 향상시키는 데 도움이 될 수 있다.
PATCH method
PATH 요청을 통해 클라이언트는 수정할 사항들을 패치 문서 형식으로 기존 리소스에 보낸다. 서버는 패치 문서를 처리하여 업데이트를 수행한다. 여기서 패치 문서는 전체 리소스를 설명하지 않고 적용할 변경 사항 집합만 설명한다. PATCH 메서드에 대한 사양(RFC 5789)은 패치 문서에 대한 특정 형식을 정의하지 않기에 요청 메시지의 MediaType에서 유추되어야 한다. (ex. JSON)
패치 문서가 JSON 형식이라고 가정해보자. 패치 문서는 원래 JSON 리소스와 동일한 구조를 갖지만 변경하거나 추가해야 하는 필드의 하위 집합만 포함한다. 또한 패치 문서에서 필드 값을 null
로 지정하여 필드를 삭제할 수 있다. 이 경우 원래 리소스가 null
값을 가질 수 있는 경우 적합하지 않을 수 있다.
아래는 PATCH 요청을 처리할 때 발생할 수 있는 몇 가지 ㅇ리반적인 오류 조건과 그에 따른 적절한 HTTP 상태코드의 예시다:
- 패치 문서 형식이 지원 되지 않을 때 → 415 (Unsupported Media Type)
- 잘못된 형식의 패치 문서 → 400 (Bad Request)
- 패치 문서는 유효하지만 변경 사항을 현재 상태의 리소스에 적용할 수 없을 때 → 409 (Conflict)
DELETE Method
DELETE 요청에 성공하면 서버는 프로세스가 성공적으로 처리되었지만 응답 메시지 body에 추가 정보가 없음을 나타내는 HTTP 상태코드 204 (No Content)로 응답해야 한다. 리소스가 존재하지 않는 경우 서버는 HTTP 404 (Not Found)를 반환할 수 있다.
데이터 필터링
하나의 URI를 통해 모든 리소스를 노출하는 것은 애플리케이션에서 정보의 하위 집한만 필요하더라도 대량의 데이터를 가져올 수 있다. 예를 들어 클라이언트에서 특정 값을 초과하는 비용이 있는 모든 주문을 찾아야 된다고 가정해보자. “/order” URI에서 모든 주문을 검색한 다음 클라이언트 측에서 이러한 주문을 필터링할 수 있다. 하지만 웹 API를 호스팅하는 서버에서 네트워크 대역폭과 처리 능력을 낭비하기 때문에 이러한 방식은 비효율적이다.
대신 API는 “/orders?minCost=n”과 같은 URI의 쿼리 문자열을 통해 필터를 전달할 수 있습니다. 웹 API는 쿼리 문자열의 minCost
매개 변수를 구문 분석 및 처리하고 서버 측에서 필터링된 결과를 반환한다.
컬렉션 리소스에 대한 GET 요청은 잠재적으로 많은 수의 항목을 반환할 수 있다. 단일 요청에서 반환되는 데이터의 양을 제한하도록 API를 설계해야 한다. 검색할 최대 항목 수와 컬렉션에 대한 시작 오프셋을 지정하는 쿼리 문자열을 고려하자. (ex. /orders?limit=25&offset=50
)
또한 서비스 거부 공격(Denial of Service attack, DoS attck)을 방어하기 위해 반환되는 항목 수에 상한선을 적용하는 것을 고려해야 한다. 클라이언트 애플리케이션을 지원하기 위해 페이지가 매겨진 데이터를 반환하는 GET 요청에는 컬렉션에서 사용 가능한 총 리소스 수를 나타내는 메타데이터도 포함되어야 한다.
References
- https://restfulapi.net/
- https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
※ 해당 글은 위의 두 문서를 번역하여 조합한 내용입니다.