API란
Application Programming Interface의 약자로 다른 종류의 애플리케이션 간의 서비스를 제공한다. API는 대상 애플리케이션의 구체적인 동작·구현 방식을 알지 못해도 통신할 수 있다
자동차를 예로 들면 자동차의 엔진을 구동하기 위해 사용자는 엔진의 어떻게 만들었는지나 어떻게 동작하는지에 대해서 몰라도 되며, 핸들과 엑셀레이터같은 인터페이스에 대한 사용 방법만 알면 자동차를 운전할 수 있다
REST란
REST(REpresentational State Transfer)는 웹 서비스에 설계에 대한 아키텍처 접근 방식으로 2000년 Roy Fielding의 논문에 의해 제안
Architectural Styles and the Design of Network-based Software Architectures
하이퍼미디어를 기반으로 분산 시스템 구축을 위한 아키텍처 스타일 - Roy Thomas Fielding
일반적으로 HTTP를 애플리케이션 프로토콜로 사용하지만, 반드시 HTTP를 사용할 필요는 없다
REST는 전체적으로 적용될 때 구성 요소 상호 작용의 확장성, 인터페이스의 일반성, 구성 요소의 독립적 배포 및 중간 구성 요소를 강조하여 상호 작용 대기 시간을 줄이고 보안을 강화하며 레거시 시스템을 캡슐화하는 일련의 아키텍처 제약 조건을 제공 합니다 - Roy Thomas Fielding
REST API 6가지 제약 조건
- 클라이언트 서버(Client-Server): 클라이언트와 서버를 분리하여 사용자 인터페이스 문제와 데이터 저장 문제를 분리. 사용자 인터페이스(클라이언트)의 이식성 향상과 서버를 단순화하고 확장성 향상
- 상태 비저장(Stateless): 서버는 요청을 이해하는 데 필요한 모든 정보를 포함해야 한다. 각 요청은 독립적이어야 하며, 서버에 저장된 컨텍스트를 활용할 수 없다
- 가시성 향상 - 서버는 요청 데이터 이외에 볼 데이터가 없음
- 신뢰성 향상 - 부분적 실패로부터 복구 작업 용이
- 확장성 향상 - 요청 간 상태 저장이 없으므로 서버 구성 요소가 리소스를 신속하게 해제
- 구현 단순화 - 서버가 요청 전체에서 리소스 사용량을 관리할 필요가 없음
- 통일된 인터페이스(Uniform Interface): 클라이언트는 표준화된 메서드와 응답 형식을 사용하여 상호작용한다
- 리소스 중심 설계(Resource-based): 리소스는 해당 리소스를 기반으로 고유하게 식별되는 URI를 가진다
- 표현을 통한 리소스 조작(Manipulation of Resources Through Representations): 표현을 사용하여 리소스의 상태를 캡처하고 구성 요소 간에 전송하여 작업을 수행
- 자기 설명 메시지(Self-descriptive Messages): 각 메시지는 메시지를 처리 방법에 대한 충분한 정보를 포함해야 한다
- 애플리케이션 상태 엔진으로서의 하이퍼미디어(Hypermedia as the Engine of Application State; HATEOAS): 클라이언트는 본문, 쿼리 문자열, 요청 헤더, 요청 URI를 통해 상태를 전달하고, 서비스는 본문, 응답 코드 및 응답 헤더를 통해 클라이언트에게 상태를 전달
- 캐시 가능성(Cacheable): 요청에 대한 응답 데이터 내에 캐싱이 가능한지 불가능한지에 대해 암시적 또는 명시적으로 라벨을 지정. 클라이언트는 캐싱 가능한 응답에 대해 캐싱 이후의 동일한 요청에 대해 응답을 재사용 가능. 응답을 캐싱하여 성능을 향상시키고 네트워크 트래픽을 줄일 수 있다
- 계층화된 시스템(Layered System): 각 구성 요소는 상호 작용하는 인접 계층만 볼 수 있고 너머를 볼 수 없도록 동작을 제한. 시스템에 대한 지식을 단일 레이어로 제한하여 전체 시스템 복잡성에 한계를 둔다.
- 선택적 제약 - 주문형 코드(Code-On-Demand): 자바스크립트와 같이 코드를 다운로드하고 실행하여 클라이언트 기능을 확장.
HATEOAS
Hypermedia as the engine of application state의 약자로 애플리케이션 상태 엔진으로서의 하이퍼텍스트이다. 각 요청에 대한 응답에는 다른 상태로 이동하기 위해 필요한 모든 정보가 포함되어야 합니다. 이는 api 문서를 매번 찾아보지 않고도 api를 사용할 수 있도록 정보를 제공합니다. 일반적으로 links
, _links
속성을 통해 링크 세트를 제공합니다. 이 링크 세트는 리소스의 상태에 따라 변경될 수 있습니다. 따라서 애플리케이션 상태에 따라 하이퍼미디어가 달라지는 HATEOAS의 의미를 가지고 있습니다
{
"id": 123,
"title": "게시글 제목",
"content": "게시글 내용",
"author": "글쓴이",
"created_at": "2024-04-21T12:00:00Z",
"updated_at": "2024-04-21T13:00:00Z",
"links": [
{
"rel": "self",
"href": "https://api.example.com/posts/123"
},
{
"rel": "edit",
"href": "https://api.example.com/posts/123/edit",
"action": "GET",
"types": ["text/xml", "application/json"]
},
{
"rel": "delete",
"href": "https://api.example.com/posts/123/delete",
"action": "DELETE",
"types": ["text/xml", "application/json"]
},
{
"rel": "comments",
"href": "https://api.example.com/posts/123/comments",
"action": "GET",
"types": ["text/xml", "application/json"]
}
]
}
페이지네이션에서 HATEOAS
next
: 다음 페이지prev
: 이전 페이지first
: 첫 페이지last
: 마지막 페이지
"links": [
{
"rel": "self",
"href": https://api.example.com/posts?page=3"
},
{
"rel": "add-post",
"href": https://api.example.com/posts",
"action": "POST",
},
{
"rel": "next",
"href": https://api.example.com/posts?page=4",
"action": "GET",
},
{
"rel": "prev",
"href": https://api.example.com/posts?page=2",
"action": "GET",
},
{
"rel": "first",
"href": https://api.example.com/posts?page=1",
"action": "GET",
}
{
"rel": "last",
"href": https://api.example.com/posts?page=10",
"action": "GET",
}
]
Self-descriptive
Media Type
Content-Type
헤더는 서버가 표현 형식을 클라이언트에게 알려줄 때 사용
서버가 해당 미디어 유형을 지원하지 않는 경우 HTTP 상태 코드 415
를 반환
Content-Type: application/json;
Accept
헤더는 클라이언트가 이해 가능한 컨텐츠 타입이 무엇인지 서버에게 알려줄 때 사용
서버가 해당 미디어 유형을 수락할 수 없는 경우 HTTP 상태 코드 406
을 반환
Link
Content-Type에서 적절한 타입을 지정해줄 수 없을 때, Link에서 rel=“profile”을 사용하여 예제에 대한 링크를 제공
Link: <https://api-documentation.org/docs/posts>; rel="profile"
REST 데이터 요소
데이터 요소 | 모던 웹에서의 예시 |
---|---|
자원(resource) | the intended conceptual target of a hypertext reference |
자원 식별자(resource identifier) | URL, URN |
표현(representation) | HTML document, JPEG image |
표현 메타데이터(representation metadata) | media type, last-modified time |
자원 메타데이터(resource metadata) | source link, alternates, vary |
제어 데이터(control data) | if-modified-since, cache-control |
자원: 정보에 대한 추상화. 이름을 지정할 수 있는 모든 정보는 리소스가 될 수 있다. 하이퍼 텍스트의 참조 대상이 될 수 있는 모든 개념
자원 식별자: 구성 요소 간의 상호 작용과 관련된 특정 리소스를 식별
표현: 표현을 사용하여 리소스의 현재 또는 의도된 상태를 캡처하여 해당 표현을 구성 요소 간에 전송함으로써 리소스에 대한 작업을 수행. 표현은 데이터, 데이터를 설명하는 메타데이터 및 경우에 따라 메타데이터를 설명하는 메타데이터로 구성
제어 데이터: 요청되는 작업이나 응답의 의미와 같이 구성 요소 간의 메시지 목적을 정의
합리적인 URI 설계
-
특정 리소스에 접근하기 위해 쿼리 스트링 대신 식별자를 사용
- ❌
/posts?id=123
- ✅
/posts/123
- ❌
-
URI에는 동사 대신 리소스를 표현하는 명사를 사용하고 http method를 사용하여 동작을 표현
- ❌
POST /update-post/123
- ✅
PATCH posts/213
- ❌
-
일반적으로 컬렉션을 참조하는 URI에는 복수 명사를 사용한다
- ❌
/post/123
- ✅
/posts/123
- ❌
-
URI에는 대문자보다는 소문자를 사용하고 하이픈(-)을 사용하여 단어를 구분
- ❌
/UserProfile
- ✅
/user-profile
- ❌
-
URI의 마지막에는 슬래시(/)를 포함하지 않습니다
- ❌
/posts/
- ✅
/posts
- ❌
-
컬렉션 표현을 그대로 사용할 필요는 없습니다. 더 적합한 표현을 사용하세요
- ❌ customer_list 리소스를 사용할 때
/customer_list
- ✅
/customers
- ❌ customer_list 리소스를 사용할 때
-
파일 확장자는 URI에 포함하지 않습니다. Accept 헤더를 사용하여 미디어 형식을 지정할 수 있습니다
- ❌
/posts/3/image.jpg
- ✅
/posts/3/image Accept: image/jpg
- ❌
모든 게시글: /posts
id가 “3”인 게시글: /posts/3
id가 “123”인 사용자가 작성한 모든 게시글: /users/123/posts
⚠️ Warning
/collection/item/collection 보다 복잡한 수준의 URI 사용을 피하기HTTP란
HyperText Transfer Protocol은 웹(W3) 상에서 정보를 주고 받기 위한 프로토콜이다. 하이퍼텍스트 링크를 사용하여 웹 페이지를 로드한다. 애플리케이션 계층 프로토콜이다. HTTP의 일반적인 흐름은 클라이언트 시스템에서 서버에 요청을 하고 서버에서는 요청을 처리하고 응답 메시지를 클라이언트에게 보낸다
HTTP 요청
- 메서드
- URL
- HTTP 버전
- HTTP 요청 헤더
- 요청 본문(선택 사항)
💡 Tip
민감한 정보는 URL에 포함하지 말고 Authorization Header에 넣어 전달HTTP Status Code
- 1xx: 정보 응답
- 2xx: 성공 응답
- 3xx: 리디렉션 메시지
- 4xx: 클라이언트 오류
- 5xx: 서버 오류
가장 많이 사용되는 상태 코드 10가지
- 200 OK: 일반적인 응답 성공
- 201 생성됨: 성공적으로 리소스 생성
- 204 콘텐츠 없음: 요청이 성공했지만 응답 본문이 비어 있음
- 400 잘못된 요청: 요청 이행 시 데이터 유효성 검사, 데이터 누락 등의 오류 발생
- 401 승인되지 않음: 유효하지 않는 인증 토큰, 액세스 권한이 없음
- 403 금지됨: 사용자에게 작업을 수행할 권한이 없거나 리소스를 사용할 수 없는 경우
- 404 찾을 수 없음: 요청한 리소스가 존재하지 않거나 숨겨졌을 때
- 405 메서드가 허용되지 않음: 요청한 URL이 존재하지만 요청한 HTTP 메서드를 사용할 수 없음
- 409 충돌: 요청을 이행하는 중 리소스 충돌이 발생
- 500 내부 서버 오류: 클라이언트가 해결할 수 없는 서버 측 예외 오류
HTTP Method
멱등성: 서버에 보낸 동일한 요청이 한 번 보낼 때와 여러 번 보낼 때가 같은 효과를 만들 때 멱등성을 가진다
안전성: 리소스를 변경하지 않는 메서드는 안전성을 가진다
Method | Request body / Response body | Safe | Idempotent |
---|---|---|---|
CONNECT | No / Yes | No | No |
DELETE | May / May | No | Yes |
GET | No / Yes | Yes | Yes |
HEAD | No / No | Yes | Yes |
OPTIONS | No / Yes | Yes | Yes |
PATCH | Yes / Yes | No | No |
POST | Yes / Yes | No | No |
PUT | Yes / No | No | Yes |
TRACE | No / Yes | Yes | Yes |
공통
- 지원하지 않는 메서드 요청 시:
405 메서드가 허용되지 않음
- 응답의
Allow
헤더 필드에 해당 리소스에서 지원하는 메서드의 리스트를 포함해야 한다
- 응답의
- 지원하지 않은 요청 본문 형식을 보냈을 때:
415(지원되지 않는 미디어 유형)
- 지원하지 않는 content-type을 가질 때:
406 (허용되지 않음)
으로 응답하고 사용 가능한 리소스 표현 목록을 메시지 본문에 포함하여 응답(클라이언트는 오류보다는 원하지 않는 형식이라도 응답을 받기를 기대할 수 있으므로 이 에러 응답이 반드시 좋은 것은 아님)
GET
지정된 URI에 대한 리소스 표현을 검색
- 성공적인 리소스 검색:
200(정상)
- ex. 성공적으로 리소스를 찾았을 때
- 요청은 이행되었지만 포함할 응답이 없는 경우:
204(콘텐츠 없음)
- ex. 특정 조건에 대한 리소스를 검색했지만 해당하는 리소스가 없을 때
- 클라이언트의 캐싱 버전이 유효함:
304(수정되지 않음)
- 리소스를 찾을 수 없는 경우:
404(찾을 수 없음)
POST
지정된 URI에 대한 리소스 생성(실제로 리소스를 생성하지 않는 작업을 트리거하는 데도 사용될 수 있다).
새 리소스를 컬렉션에 포함하고 URI를 할당.
새로운 리소스의 URI를 응답의 Location
헤더에 포함하고 리소스 표현을 본문에 포함
- 성공적인 리소스 생성:
201(생성됨)
- 리소스를 생성했지만 반환할 결과가 없는 경우:
204(콘텐츠 없음)
- 클라이언트가 잘못된 요청 데이터를 입력:
400(잘못된 요청)
- form 입력 값 검증 실패 - > 오류에 대한 정보나 링크를 응답 본문에 포함하여 제공
- 리소스가 이미 존재하는 경우:
409(충돌)
- 지원하지 않은 요청 본문 형식을 보냈을 때:
415(지원되지 않는 미디어 유형)
PUT
지정된 URI에 대한 리소스를 생성 또는 대체. 클라이언트는 리소스에 대한 URI를 지정하고 요청 본문에 리소스의 완전한 표현을 포함. URI에 해당하는 리소스가 이미 존재하는 경우 대체한다. 해당하는 리소스가 없는 경우 서버가 지원한다면 새로운 리소스를 생성할 수 있다. 여러 리소스에 대해 일괄 업데이트를 수행하는 작업을 구현할 수 있다
- 성공적인 리소스 생성:
201(생성됨)
- 기존 리소스 업데이트:
200(정상)
- 리소스 생성 또는 업데이트에 성공했지만 반환할 결과가 없는 경우:
204(콘텐츠 없음)
- 리소스를 찾을 수 없는 경우:
404(찾을 수 없음)
- 지원하지 않은 요청 본문 형식을 보냈을 때:
415(지원되지 않는 미디어 유형)
- 기존 리소스 업데이트 실패:
409(충돌)
PATCH
PATCH
: 지정된 URI에 대한 리소스 부분 수정. 클라이언트는 리소스에 대한 URI를 지정하고 요청 본문에는 리소스에 대한 변경 사항의 집합을 포함. 전체 표현을 제출하는 PUT과 다르게 변경 사항만 제출
- 성공적인 리소스 수정:
200(정상)
- 리소스 생성 또는 업데이트에 성공했지만 반환할 결과가 없는 경우:
204(콘텐츠 없음)
- 지원하지 않은 요청 본문 형식을 보냈을 때:
415(지원되지 않는 미디어 유형)
- 잘못된 패치 문서:
400(잘못된 요청)
- 리소스를 찾을 수 없는 경우:
404(찾을 수 없음)
- 패치 문서는 유효하지만 변경 사항을 적용할 수 없음:
409(충돌)
https://datatracker.ietf.org/doc/html/rfc5789
패치 문서 형식
일반적으로 사용하는 JSON에서는 JSON Merge Patch 과 JSON Patch라는 두 가지 주요 패치 방식이 있다
-
JSON Merge Patch: RFC 7386에 정의된 JSON 문서의 변경 사항을 나타내는 형식. 원래의 JSON 리소스 구조와 동일하고 변경되거나 추가될 필드만을 포함. null값을 사용하여 필드를 삭제할 수 있다. 명시적으로 null 값을 사용하지 않는 json 문서에서 수정 사항을 설명하기 적합하다. 미디어 유형은
application/merge-patch+json
이다 예시: a 값을 “b” -> “z”로 변경하고 f를 제거하는 방법{ "a": "b", "c": { "d": "e", "f": "g" } }
{ "a": "z", "c": { "d": "e", "f": "null" } }
-
JSON Patch: RFC 6902에 정의된 JSON 문서의 변경 사항을 나타내는 형식. 변경 사항에 대한 작업을 명시적으로 나열한다. 변경사항은 op(동작)과 path(경로), 값(value)로 이루어진다. 동작은
add
,remove
,replace
,move
,copy
,test
가 있다. 미디어 유형은application/json-patch+json
이다동작 설명 add 대상 위치의 내용에 따라 달라진다. (배열의 인덱스) 배열의 지정된 인덱스에 새 값 삽입, (존재하지 않는 개체 지정) 새로운 개체 추가,(존재하는 개체 지정) 해당 개체의 값 대체 remove 대상 위치의 값 제거 replace 대상 위치의 값을 새 값으로 대체 move 지정된 위치의 값을 제거하고 대상 위치에 추가 (remove + add와 동일) copy 지정된 위치의 값을 대상 위치로 복사 test 대상 위치의 값이 지정된 값과 같은지 테스트 예시:
[ { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }, { "op": "remove", "path": "/a/b/c" }, { "op": "replace", "path": "/a/b/c", "value": 42 }, { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }, { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }, { "op": "test", "path": "/a/b/c", "value": "foo" } ]
DELETE
지정된 URI에 대한 리소스 제거. 일반적으로 요청 본문은 비어 있다
- 성공적인 리소스 수정:
200(정상)
- 리소스 제거에 성공했지만 반환할 결과가 없는 경우:
204(콘텐츠 없음)
- 삭제해야 할 리소스가 존재하지 않음:
404(찾을 수 없음)
리소스 | POST | GET | PUT | DELETE |
---|---|---|---|---|
/posts | 새 게시글 만들기 | 모든 게시글 검색 | 게시글 일괄 업데이트 | 모든 게시글 삭제 |
/posts/1 | X | id가 1인 게시글의 세부 정보 검색 | id가 1인 게시글 업데이트 | id가 1인 게시글 삭제 |
비동기 작업
작업이 오래 걸리는 경우 클라이언트가 응답을 받을 때까지 기다리는 것은 많은 지연 시간을 발생시키는 원인이 됩니다. 이 문제를 해결하기 위해 작업을 비동기 형태로 만들 수 있습니다. 요청이 수락되면 상태 엔트포인트를 응답으로 받고, 클라이언트는 상태 엔드포인트를 주기적으로 확인하여 상태를 모니터링할 수 있습니다
HTTP/1.1 202 Accepted
Location: /api/v1/status/123
/api/v1/status/123
에 주기적으로 GET 요청을 보내 상태를 모니터링
HTTP/1.1 200 OK
Content-Type: application/json
{
"status":"progress",
"link": { "rel":"cancel", "method":"delete", "href":"/api/v1/status/123" }
}
만약 비동기 작업이 새 리소스를 생성하는 경우 상태 엔드포인트에서는 작업이 완료된 후 HTTP 상태 코드 303(기타 참조)
를 반환해야 합니다
HTTP/1.1 303 See Other
Location: /api/v1/posts/123
대규모 바이너리 리소스를 위한 부분 응답 지원
대규모 바이너리 필드를 안전하게 전달하기 위해 청크 단위의 검색을 지원할 수 있습니다
리소스 정보 가져오기
HTTP HEAD 요청을 지원하여 리소스에 대한 설명을 제공하세요
이 HEAD 요청에 대한 응답은 빈 응답 본문과 함께 리소스를 설명하는 헤더만 제공합니다
HEAD /posts/123?fields=image HTTP/1.1
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 3240
- Accept-Ranges: 부분 응답 지원을 클라이언트에게 알리기 위해 사용
- Content-Type: 리소스의 미디어 타입
- Content-Length: 리소스의 전체 크기
리소스 부분 요청
Range 헤더를 사용해 리소스에 대해서 부분 응답을 요청합니다
GET /posts/123?fields=image HTTP/1.1
Range: bytes=0-1000
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 1000
Content-Range: bytes 0-1000/3240
[...]
API Versioning
이미 API를 사용하고 있는 애플리케이션이 있을 때 API를 중단시키지 않고 API를 변경하기 위한 버전 관리
개발자는 자신의 일정에 따라 선택적으로 api 버전을 업그레이드하고, 이전 버전은 여전히 사용 가능
URI 경로 사용
https://example.com/api/v1/posts
https://example.com/api/v2/posts
버전이 달라도 가져오는 리소스는 동일하므로 버전에 따라 URI가 달라져서는 안됩니다
쿼리 문자열 사용
https://example.com/api/posts?version=1
https://example.com/api/posts?version=2
Accept 헤더 사용
GET https://example.com/api/posts HTTP/1.1
Accept: version=1
커스텀 헤더 사용
GET https://example.com/api/posts HTTP/1.1
X-API-Version: 1.0
일반적인 응답 구조
{
"statusCode": "...",
"message": "...",
"data": [...],
"meta": {...},
"error": {...}
}
Pagination
데이터 목록을 보여줄 때는 모든 데이터를 한 번에 주지 않도록 페이지네이션을 사용해야 한다
페이지네이션을 사용하는 이유
- 한번에 너무 많은 항목을 요청하면 데이터베이스부터 백엔드, 클라이언트까지 과부하가 걸립니다. 이는 성능 저하를 유발할 수 있습니다
- 가장 큰 이유이다. 예를 들어 상품 레코드가 수십만개 이상 될 때 상품 목록 페이지에서 매번 이를 모두 가져오는 것은 매우 큰 낭비이고 비용 증가로 이어진다
- 사용자는 전체 데이터 중에서 일부만을 원할 가능성이 큽니다. 이는 사람들이 검색 결과에서 최상단의 콘텐츠 혹은 검색결과 첫 페이지에만 집중하는 것과 같습니다. 데이터가 더 필요하다면 그 때 요청하면 됩니다.
- 이는 클라이언트 측에서 필터링과 같은 작업을 통해 해결할 수 있는 문제이지만, 낮은 성능의 디바이스에는 이 조차도 어려울 수 있다
클라이언트 관점에서 페이지네이션
페이지로 나누기
가장 일반적인 페이지네이션으로 일반적으로 1페이지부터 시작해 페이지 목록을 나열해 놓는 방법이다
✅ 현재 보고 있는 검색 결과 페이지를 직관적으로 알 수 있고 다시 접근하기 쉬움
❌ 다른 검색결과를 보기 위해 조작할 수 있는 방법이 많음(이전 버튼, 다음 버튼, n페이지 버튼 등)
❌ 다른 검색결과를 보기 위해 페이지를 이동해야 함
더 가져오기
다음 검색 결과가 이전 검색 결과 이후에 추가 되는 형태
✅한 페이지 내에서 검색 결과를 계속 볼 수 있음
✅현재 검새 결과의 총 크기를 알 수 있음
❌검색 결과가 너무 많은 경우 다루기 어려움
무한 스크롤
”더 가져오기” 형태에서 다음 검색 결과를 가져오는 버튼 없이 스크롤 시에 자동으로 가져오는 방식
✅다음 검색 결과를 보기 위해 사용자가 아무 버튼을 조작하지 않아도 됨
❌검색 결과가 너무 많은 경우 다루기 어려움
💡 Tip
구글 검색 센터의 페이지네이션 가이드를 참고하세요.매우 많은 수의 리소스 요청을 하지 못하도록 반환되는 항목수(limit)에 상한을 적용해야 한다.
해당 컬렉션에서 사용 가능한 총 리소스 수(total)를 응답에 포함
"pagination": {
"totalItems": 20,
"currentPage": 1,
"totalPages": 2,
"nextPage": 2,
"prevPage": null
}
Offset(Page) based pagination
가장 일반적인 페이지네이션 방법
1. offset
와 limit
를 사용하는 방법
- offset -> 건너 뛸 레코드 수
/posts?offset=10&limit=10
2. page
와 limit
를 사용하는 방법
page를 offset으로 변환하는 단계만 추가된 것으로 offset 방식과 거의 동일
- offset = (page - 1) * size
/posts?page=1&size=10
응답 데이터 예시
// result
{
"data": [...],
"pagination": {
"currentPage": 1,
"prevPage": null,
"nextPage": 2,
"hasNextPage": true,
"hasPrevPage": false,
"totalPages": 57,
"pageSize": 10,
"totalItems": 566
}
}
offset 방식의 장점
-
사용자 친화적 - api를 호출하는 사용자는 직관적으로 특정 위치로 이동할 수 있다
-
구현이 쉬움 - 데이터베이스에서 쉽게 구현할 수 있다.
SELECT * FROM posts ORDER BY id LIMIT 10 OFFSET 10;
offset 방식의 단점
- 레코드를 탐색하는 도중 레코드의 삭제와 같은 이유로 페이지 번호가 바뀔 수 있다. 이로 인해 아래와 같은 문제가 발생할 수 있다
- 같은 항목이 두 번 표시됨 - 페이지 이동 간에 항목의 순서가 변경되어 1페이지의 마지막에서 봤던 항목이 2페이지의 처음 항목으로 다시 나타날 수 있음. 특히
offset
방식으로 무한 스크롤을 구현할 경우 유의해야 함 - 항목이 건너뛰어짐 - 위의 상황과 반대로 1페이지에서 2페이지로 이동할 때 2페이지 첫번째였던 항목이 1페이지 마지막으로 이동하여 1페이지와 2페이지 모두에서 항목을 지나칠 수 있음
- 같은 항목이 두 번 표시됨 - 페이지 이동 간에 항목의 순서가 변경되어 1페이지의 마지막에서 봤던 항목이 2페이지의 처음 항목으로 다시 나타날 수 있음. 특히
- 탐색 비용 증가 - 레코드가 많아 질수록 건너뛰어야 할 레코드 수가 증가하기 때문에 탐색 비용이 증가한다
Cursor based pagination
ID와 같은 레코드의 고유 식별자를 커서로 활용하여 페이지네이션의 시작점을 정하는 방법
count
및 after
또는 next
를 사용
- count -> 가져올 레코드 수
- after, next -> 해당 레코드 다음부터 레코드를 가져오기 위해 사용하는 커서 값
/posts?after=23786347824&count=10
/posts?next=23786347824&count=10
응답 데이터 예시
// result
{
"data": [...],
"pagination": {
"hasNextPage": true,
"nextCursor": "23786347863",
"hasPrevPage": true,
"prevCursor": "23786347810"
}
}
cursor 방식의 장점
무한 스크롤에 적합 - 탐색 중에 특정 레코드가 삭제되어 밀리는 경우 생기는 문제(offset 기반의 문제점인 “두번 표시” 또는 “표시 되지 않음”)를 해결할 수 있다
cursor 방식의 단점
직접 이동 어려움 - 커서 값을 알 수 없기 때문에 27페이지 와 같이 명시적으로 특정 위치로 이동하기 어려움
구현 난이도 - 일반적으로 offset 기반 페이지네이션보다 복잡하고 구현하기 어려움
Time based pagination
start_time
와 end_time
와 같은 시간을 특정하여 사용
/posts?start_time=2024-04-01T00:00:00Z&end_time=2024-05-01T00:00:00Z
Key based pagination
last_key
와 같은 특정 키를 사용
/posts?last_key=3
Filtering
-
querystring은 기본적으로 필터링에 사용된다
-
리소스 필터링:
?filter=color:red
빨간 색 속성을 가지는 리소스만 필터링
https://api.example.com/clothes?filter=color:red
https://api.example.com/clothes?min_price=10000
- 필드 필터링:
?fields=id,name
: id,name 필드만 필터링하여 가져옴
https://api.example.com/clothes?fields=id,name
Sorting
?sort_by=price
가격 순으로 리소스를 정렬
https://api.example.com/clothes?sort_by=price&order_by=asc
[sort]:[order]
sort를 key로, order를 value로 사용한 키 값쌍으로 정렬
https://api.example.com/clothes?sort=price:asc
Searching
https://api.example.com/clothes?query=shirt
Default value
각각의 선택적 매개변수에 대해서 적절한 기본값을 지정해야 한다
예시
-
offset -> 0
-
page -> 1
-
limit -> 10
-
sort_by -> id
-
order_by -> asc
-
fields -> 모든 필드
Error
{
"error": {
"status": 401,
"errorCode": "ERROR401-01"
"message": "Unauthorized"
}
}
TLS 암호화
민감한 자격 증명은 Authorization
와 같은 HTTP 헤더를 사용하여 전달
TLS 암호화를 통해 엔드투엔드 전송을 보호
API rate limit
-
API Gateway 서비스를 사용하여 응답을 캐싱하고 속도를 제한
-
시간당 요청 제한: 분당 요청 수를 제한하고 그 이상의 요청에 대해서는
429(너무 많은 요청)
응답을 반환- 초당 요청 수(RPM)
- 분당 요청 수(RPD)
- 초당 요청 수(RPS)
-
동시 접속 제한(Concurrency limit): 동시 접속 수를 제한하고 그 이상의 요청에 대해서는
429(너무 많은 요청)
응답을 반환할 수 있다 -
Retry-After
헤더를 사용하여 새로운 요청을 보내기 위해 기다려야 하는 시간을 알려줄 수 있다HTTP/1.1 429 Too Many Requests Content-Type: text/html Retry-After: 3600
Authentication
Basic 인증
요청의 Authorization
헤더에 아이디와 비밀번호를 base64로 인코딩한 문자열을 포함
아이디: abcd1234
, 비밀번호 abcd1234
-> abcd1234:abcd1234를 base64로 인코딩
HTTPS 사용이 필수적
Authorization: Basic YWJjZDEyMzQ6YWJjZDEyMzQ=
API 키 인증
-
쿼리 문자열
GET /api/resource?api_key=w3jfnrei-d1g5-6htq-5hwg-43g3ka43k
-
X-Api-Key
헤더GET "https://example.com/api/resource" X-Api-Key: w3jfnrei-d1g5-6htq-5hwg-43g3ka43k
-
Bearer 토큰
GET "https://example.com/api/resource" Authorization: Bearer w3jfnrei-d1g5-6htq-5hwg-43g3ka43k
-
요청 본문
POST "https://example.com/api/resource" { "api_key": "w3jfnrei-d1g5-6htq-5hwg-43g3ka43k" }
Bearer 인증
베어러 토큰의 형식으로는 JWT
이 널리 사용된다
GET "https://example.com/api/resource"
Authorization: Bearer w3jfnrei-d1g5-6htq-5hwg-43g3ka43k
OAuth 2.0
Open Authorization 2.0은 인증을 위해서 널리 사용되는 업계 표준 프로토콜이다
https://developers.google.com/identity/protocols/oauth2?hl=ko
Cache
응답이나 요청에 대한 데이터를 저장하고 동일한 응답이나 요청에 대해 이를 재사용한다. 서버에서는 DB I/O, 네트워크 트래픽을 줄일 수 있고 클라이언트에서는 네트워크 트래픽을 줄이거나 요청 자체를 하지 않음으로써 지연 시간을 감소시키는 효과를 가진다. 하지만 캐싱으로 인해 오래되거나 신선하지 못한 데이터를 제공할 수 있어 적절한 캐싱 만료 및 검증 전략을 만들어야 한다
캐시의 종류
- 브라우저 캐시(개인 캐시): 사용자의 웹 브라우저에 저장되는 캐시
- 프록시 캐시(공유 캐시): ISP나 대기업에서 배포하고 관리하는 캐시. 클라이언트와 서버 중간에서 중계 기능을 하고 요청과 응답을 임시로 저장하여 캐싱
- 게이트웨이, 역방향 프록시 캐시: 서버 앞에 위치하여 네트워크 트래픽을 줄이고 자주 사용되는 콘텐츠를 저장
캐싱의 장점
- 많은 경우 클라이언트는 요청을 보낼 필요가 없음 - 지연 시간 및 네트워크 대역폭 감소
- 많은 경우 서버는 요청을 처리할 필요가 없음 - 서버 운영에 대한 워크로드 감소
캐싱 대상
HTTP 캐시는 일반적으로 GET
응답에 대해서만 캐싱
캐싱 방법
서버에서 응답에 대해 올바른 캐싱 헤더를 지정하여 클라이언트가 캐시하도록 지정
- 브라우저가 이 응답을 캐싱해야 하는가?
- 얼마 동안 캐싱해야 하는가?
- 서버의 확인 없이 캐시의 데이터로 사용해도 괜찮은가?
- 캐시에 있는 데이터가 만료될 경우 어떻게 해야 하는가?
Expires
리소스가 캐시되는 기간을 알려준다
Expires: Thu, 25 Apr 2024 06:36:06 GMT
Cache-Control
-
max-age
: 캐시의 데이터가 최신 상대로 간주될 수 있는 최대 시간(초)를 명시Cache-Control: max-age=3600
-
s-maxage
: 공유/프록시 캐시를 대상으로max-age
최대 시간을 명시Cache-Control: s-maxage=3600
-
public
: 브라우저, 공유·프록시 등의 모든 종류에 대해 응답이 캐시 될 수 있음을 명시 -
private
: : 개인 캐시에 대해서만 응답을 캐싱할 수 있음을 명시 -
no-store
: 응답이 전혀 캐시되지 않아야 한다. 캐싱되면 안되는 민감한 데이터일 경우 사용 -
no-cache
: 응답을 캐싱할 수 있지만 사용 전 서버로부터 검증이 필요 -
must-revalidate
: 캐시된 데이터가 오래될 경우 서버에서 최신 상태를 확인하여 절대 오래된 버전의 응답을 사용해서는 안된다는 것을 명시 -
proxy-revalidate
: 공유/프록시 캐시를 대상으로must-revalidate
지정
캐싱 만료
Expires
, max-age
를 사용하여 캐싱의 만료 시간을 지정
캐싱 검증
ETag
, Last-Modified
를 사용하여 캐시의 데이터가 오래되었는지 확인
ETag: 리소스의 버전을 식별하기 위한 태그로, 문자열을 사용. 일반적으로 응답 본문의 해시로 만들어지며, 따라서 응답 본문이 변경될 때마다 ETag의 값이 함께 변경된다
ETag: "8a6e899e42889e19a319c04b1c89dd98"
Last-Modified: 리소스가 서버에서 마지막으로 수정된 날짜와 시간을 확인
Last-Modified: Thu, 25 Apr 2024 06:36:06 GMT
-
➡️클라이언트에서 리소스에 대해 처음 요청
GET /api/resource HTTP/1.1
-
⬅️서버는 Cache-Control, ETag, Last-Modified 헤더와 함께 200 응답
HTTP/1.1 200 OK Cache-Control: max-age=60 Last-Modified: Thu, 25 Apr 2024 06:36:06 GMT ETag: "8a6e899e42889e19a319c04b1c89dd98" ...
-
➡️이후 60초 동안 클라이언트가 하는 같은 API 엔드포인트의 요청에 대해 브라우저 캐시를 통해 서버에 대한 요청없이 데이터 사용(네트워크 대역폭 절약)
-
➡️60초 후 캐시가 만료 된 경우 새로운 요청 시 클라이언트는 서버에 요청
GET /api/resource HTTP/1.1 If-None-Match: "8a6e899e42889e19a319c04b1c89dd98" If-Modified-Since: Thu, 25 Apr 2024 06:36:06 GMT
-
⬅️리소스가 수정된 경우 일반적인 응답처럼 리소스와 함께 200 응답
-
⬅️리소스가 수정되지 않은 경우 응답 본문 없이 304 응답(네트워크 대역폭 절약)
Wire model vs storage model
Wire model(와이어 모델)
서버 ↔ 클라이언트 통신 시에 사용하는 데이터 모델
: 이 용어는 일반적으로 네트워크 통신을 위해 데이터를 표현하는 모델을 가리킵니다. 데이터를 전송하기 위해 사용되는 프로토콜에 따라 데이터의 형식이 결정됩니다. 예를 들어, 웹 서비스에서는 JSON(JavaScript Object Notation)이나 XML(Extensible Markup Language) 형식으로 데이터를 전송하는 것이 일반적입니다. 이런 형식들은 데이터를 전송하는 데 사용되는 ‘와이어’나 네트워크 상에서의 통신을 위한 모델을 정의합니다.
Storage model(스토리지 모델)
서버 ↔ 데이터베이스 통신 시에 사용하는 데이터 모델
이 용어는 데이터베이스에서 데이터를 저장하는 방식을 설명합니다. 데이터베이스 관리 시스템(DBMS)은 데이터를 디스크에 저장하고, 필요할 때 검색하고 조작하는 방법을 결정해야 합니다. 이러한 스토리지 모델은 데이터베이스 시스템의 성능, 안정성, 확장성 등에 영향을 미칩니다. 스토리지 모델에는 관계형 데이터베이스의 테이블, NoSQL 데이터베이스의 문서, 키-값 쌍, 그래프 등이 포함될 수 있습니다.
애플리케이션 아키텍처(Layered Architecture)
microsoft REST API 가이드 라인 https://github.com/Microsoft/api-guidelines
google API 디자인 가이드 https://cloud.google.com/apis/design
salesforce api 문서 https://developer.salesforce.com/docs/apis
x(구 twitter) api 문서 https://developer.twitter.com/en/docs/api-reference-index
wix의 rest api 문서 https://dev.wix.com/docs/rest
wordpress rest api 문서 https://developer.wordpress.com/docs/api/
strapi rest api 문서 https://docs.strapi.io/dev-docs/api/rest
cafe24 rest api 문서 https://developers.cafe24.com/docs/api/admin/
github rest api 문서 https://docs.github.com/en/rest
kakao rest api 문서 https://developers.kakao.com/docs/latest/ko/rest-api/getting-started
google api 개선 제안 https://google.aip.dev/