REST API 설계 베스트 프랙티스
REST API 설계 베스트 프랙티스
서론
REST API는 웹 서비스의 핵심 구성 요소로, 잘 설계된 API는 개발자 경험을 향상시키고 시스템의 유지보수성을 높입니다. 이 글에서는 REST API를 설계할 때 고려해야 할 핵심적인 베스트 프랙티스에 대해 알아보겠습니다.
1. REST의 핵심 원칙 (Core Principles)
REST(Representational State Transfer)는 Roy Fielding의 박사 학위 논문에서 제안된 아키텍처 스타일로, 일련의 제약 조건(constraints)을 기반으로 합니다.
그중 주요 제약 조건은 다음과 같습니다.
클라이언트-서버 (Client-Server)
클라이언트와 서버는 관심사가 분리된 구조로 구성되며, 일반적으로 HTTP를 통해 요청과 응답을 주고받습니다.
무상태성 (Stateless)
서버는 클라이언트의 상태를 저장하지 않으며, 각 요청은 독립적으로 처리 가능해야 하고 필요한 모든 정보를 포함해야 합니다.
캐시 가능성 (Cacheable)
HTTP Cache-Control 헤더를 사용하여 클라이언트나 중간 서버(프록시)에서 응답을 캐싱할 수 있으며, 네트워크 효율을 향상시킬 수 있습니다.
자체 표현 구조 (Self-descriptive)
각 메시지는 별도의 외부 정보 없이도 스스로 해석 가능해야 하며, Content-Type과 같은 메타데이터를 통해 데이터 형식이 명확히 표현되어야 합니다.
2. 리소스 식별자: URI, URL, URN
리소스는 웹에서 고유하게 식별되며, 일반적으로 URI를 통해 표현됩니다.
URI (Uniform Resource Identifier)
리소스를 식별하는 표준적인 체계이며, scheme:[//authority]path[?query][#fragment] 형태로 구성됩니다.
URL (Uniform Resource Locator)
리소스를 식별하는 동시에 해당 리소스에 접근할 수 있는 위치 정보를 포함합니다. 우리가 일반적으로 사용하는 웹 주소가 이에 해당합니다.
URN (Uniform Resource Name)
리소스의 위치와 무관하게 부여되는 고유한 이름입니다. (예: urn:isbn:0451450523)
참고: REST API 설계에서는 리소스를 식별하는 URI의 path 구조가 핵심이며, API 엔드포인트 설계의 기준이 됩니다.
3. 리소스 네이밍: 동사가 아닌 명사 사용하기
RESTful API에서는 엔드포인트 경로에서 리소스의 이름을 지정할 때 동사가 아닌 명사를 사용해야 합니다. HTTP 메서드가 이미 동사를 포함하고 있기 때문에, 리소스 이름은 명사로 표현하는 것이 좋습니다.
예시:
GET /licenses(O) - 라이선스 상태를 전송받음GET /getLicenses(X) - 처리 지침을 전달하는 RPC 스타일
위의 예시에서는 CRUD 오퍼레이션 중 읽기 오퍼레이션만 보여주고 있습니다. 나머지 오퍼레이션들을 설계한다면 다음과 같은 엔드포인트 구조가 될 것입니다:
POST /licenses: 새 라이선스를 생성합니다.GET /licenses/{license_key}: 특정 라이선스 정보를 조회합니다.PATCH /licenses/{license_key}: 특정 라이선스의 일부를 업데이트합니다.- 여기서
{license_key}는 경로 파라미터로, 라이선스 컬렉션 내에서 고유한 식별자 역할을 합니다. - 각 라이선스는 고유한 키를 가지며, 이 호출은 주어진 라이선스에 대한 업데이트를 수행합니다.
- 참고: GitHub는 리소스 전체를 교체할 때
PUT을 사용합니다.
- 여기서
DELETE /licenses/{license_key}: 특정 라이선스를 삭제합니다.
POST는 일반적으로 리소스 생성이나 서버 처리에 사용되지만, 검색 용도로도 사용할 수 있습니다.
- query string으로 표현하기 어려운 계층 구조
- 요청 데이터를 URL에 포함하는 방식이 적합하지 않은 경우
4. 컬렉션 리소스명은 복수형 사용하기
엔드포인트 경로에서 컬렉션을 나타내는 리소스명은 항상 복수형을 사용해야 합니다. 이는 RESTful API에서 일반적으로 채택된 컨벤션으로, 컬렉션과 개별 항목을 명확히 구분하는 데 도움이 됩니다.
예시:
GET /licenses(컬렉션 조회)GET /licenses/{id}(개별 항목 조회)
5. HATEOAS (Hypermedia as the Engine of Application State)
HATEOAS는 API 응답에 다른 리소스에 대한 링크를 포함하여, 클라이언트가 서버가 제공하는 링크를 따라 API를 사용할 수 있도록 하는 방식입니다.
- 장점: 클라이언트가 API 경로를 미리 하드코딩하지 않고, 서버가 제공하는 링크를 통해 필요한 리소스에 접근할 수 있어 API 변경에 대한 유연성이 높아집니다.
6. API 버전 관리
API 버전 관리는 하위 호환성을 유지하면서 API를 발전시키는 데 필수적입니다. API는 시간이 지남에 따라 지속적으로 개선되지만, 기존 버전을 사용하는 사용자가 존재할 수 있기 때문에 여러 버전의 API를 제공해야 하는 경우가 있습니다.
API 버전 관리에는 여러 방법이 있습니다:
헤더를 통한 버저닝
Accept: application/vnd.github.v3+json
이 접근 방식의 장점은 기본 버전을 설정할 수 있다는 점입니다. Accept 헤더가 없는 경우 기본 버전으로 응답할 수 있습니다. 다만, 클라이언트가 헤더를 명시적으로 관리해야 하므로, 클라이언트 구현이 상대적으로 복잡해질 수 있습니다.
엔드포인트 경로를 통한 버저닝
https://api.example.com/v1/resource
URL 자체에 버전을 포함하므로, 클라이언트는 항상 의도한 API 버전을 명시적으로 사용하게 됩니다. 이 방식은 기본 버전을 제공하지 않지만, 요청 포워딩과 같은 방법을 통해 제한을 극복할 수 있습니다.
두 방식 모두 장단점이 있으며, 시스템 특성과 클라이언트 환경에 따라 적절한 방식을 선택하는 것이 중요합니다.
7. 중첩된 리소스 (Nesting)
관계가 있는 리소스는 중첩된 구조로 표현할 수 있습니다. 이는 리소스 간의 계층 관계를 명확하게 보여주며, API의 직관성을 높여줍니다.
중첩된 리소스 접근
일반적으로 상위-하위 관계가 명확하고, 하위 리소스가 상위 리소스의 컨텍스트 안에서만 의미가 있는 경우 중첩된 구조를 사용합니다.
예시: 고객과 주소
GET /customers/1/addresses(고객 1의 모든 주소 조회)POST /customers/1/addresses(고객 1에 새 주소 추가)GET /customers/1/addresses/2(고객 1의 2번 주소 조회)
그러나 리소스가 여러 컨텍스트에서 재사용되거나, 자체적인 정체성을 가진 경우에는 독립적인 리소스로 접근하는 것이 더 적절할 수 있습니다.
-
리소스가 여러 도메인에서 재사용되는 경우
- 주소는 고객뿐만 아니라 주문, 배송지 등 여러 곳에서 참조될 수 있습니다.
- 이럴 때는 주소를 독립적인 리소스로 관리하는 것이 더 효율적입니다.
-
마이크로서비스 아키텍처에서의 분리
- 주문과 결제가 서로 다른 도메인 서비스로 분리된 경우, 각 서비스는 자체적인 리소스 경로를 가져야 합니다.
두 방식을 HATEOAS와 결합하면 클라이언트가 동적으로 API를 탐색할 수 있습니다. 예시: HATEOAS 응답
{
"id": 123,
"customerId": 1,
"status": "PAID",
"amount": 15000,
"_links": {
"self": {
"href": "/orders/123",
"method": "GET"
},
"update": {
"href": "/orders/123",
"method": "PUT"
},
"customer": {
"href": "/customers/1",
"method": "GET"
},
"payments": {
"href": "/payments?orderId=123",
"method": "GET"
}
}
}
6. API 보안
안전한 API를 설계하기 위한 권장사항은 다음과 같습니다:
- 암호화된 통신
- HTTPS 사용: 암호화된 통신을 위해 항상 HTTPS 사용해야 합니다.
-
OWASP API 보안 위협 대응
- OWASP의 주요 API 보안 위협 및 취약점을 살펴보고 대응해야 합니다.
- API 보안 확인 사이트:
-
인증 및 인가
- 상태 비저장(stateless) 인증: REST API는 상태를 유지하지 않는(stateless) 특성을 가지므로, 세션이나 쿠키 대신 JWT(JSON Web Tokens)나 OAuth 2.0 기반 토큰을 사용한 인증 방식을 구현해야 합니다.
7. 문서화
- 항상 최신 버전의 API 문서 유지
- 샘플 코드와 예제 제공
- 변경 이력 및 사용 중단(deprecation) 공지
- 버전별 변경 사항 상세 설명
8. 권장되는 상태 코드 준수
일반적으로 사용되는 REST 응답 상태 코드를 정리한 목록입니다. 상태 코드 전체 목록은 RFC 7231을 참고한다.
성공적인 응답 (2xx)
| 상태 코드 | 설명 |
|---|---|
200 OK | 요청이 성공적으로 처리됨 |
201 Created | 새로운 리소스가 성공적으로 생성됨 |
202 Accepted | 요청이 접수되었으나 처리가 완료되지 않음. 서버가 요청을 수락했지만 일괄 처리와 같이 즉시 응답을 보낼 수 없는 경우 (비동기 처리) |
204 No Content | 요청이 성공적으로 처리되었으며, 응답 본문은 포함되지 않음. (예 성공 등) |
리다이렉션 (3xx)
| 상태 코드 | 설명 |
|---|---|
304 Not Modified | 리소스가 변경되지 않았음을 알리고 본문 없이 응답 |
클라이언트 오류 (4xx)
| 상태 코드 | 설명 |
|---|---|
400 Bad Request | 매개변수가 올바르지 않거나 누락되었거나 요청이 잘못되어 처리할 수 없는 경우 |
401 Unauthorized | 인증 실패(Unauthenticated) |
403 Forbidden | 권한 없음 |
404 Not Found | 요청한 리소스를 찾을 수 없음 (존재하지 않는 경로 또는 삭제된 리소스) |
405 Method Not Allowed | 요청한 리소스에 대해 해당 HTTP 메서드가 허용되지 않아 실패한 상태 |
409 Conflict | 리소스 상태 충돌 (예: 이미 존재하는 리소스 생성 시도, 참조 무결성 위반) |
429 Too Many Requests | 과도한 요청으로 인한 요청 한도 초과 |
서버 오류 (5xx)
| 상태 코드 | 설명 |
|---|---|
500 Internal Server Error | 서버 내부 오류 |
502 Bad Gateway | 업스트림 서버 호출이 실패 |
503 Service Unavailable | 서버에서 예상치 못한 일이 발생하여 실패(과부하 혹은 서비스 실패 등) |
9. 캐싱 보장
HTTP는 캐시를 활용할 수 있는 메커니즘을 제공한다. 클라이언트는 응답으로 받은 캐시 관련 헤더를 기반으로, 서버에 다시 요청할 때 리소스가 변경되었는지 확인하고 캐시 사용 여부를 결정한다.
대표적으로 아래 두 가지 방식이 있다.
ETag 사용
ETag(Entity Tag)는 리소스의 버전을 식별하는 고유한 해시 값 또는 체크섬 값으로, 리소스의 응답이 변경될 때마다 함께 변경됩니다. 클라이언트는 이 값을 활용하여 캐시된 리소스가 최신인지 확인할 수 있다.
작동 방식:
- 서버는 리소스와 함께 ETag 헤더를 응답으로 전송합니다.
- 클라이언트는 이후 요청 시
If-None-Match헤더에 이 ETag 값을 포함시킵니다. - 서버는 현재 리소스의 ETag와 비교하여:
- ETag가 일치하면
304 Not Modified상태 코드로 응답하여 캐시를 재사용하도록 합니다. - ETag가 다르면 새로운 리소스와 함께 업데이트된 ETag를 반환합니다.
- ETag가 일치하면
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified 헤더 사용
Last-Modified는 리소스가 마지막으로 수정된 시점(RFC-1123 형식)을 기준으로 캐시 유효성을 확인하는 방식입니다. ETag보다 단순하지만, 시간 기반이기 때문에 변경 여부를 정확히 표현하는 데 한계가 있어 보조적으로 사용되는 경우가 많습니다.
작동 방식:
- 서버는 리소스와 함께 Last-Modified 헤더를 응답으로 전송합니다.
- 클라이언트는 이후 요청 시
If-Modified-Since헤더에 이 타임스탬프 값을 포함시킵니다. - 서버는 리소스의 마지막 수정 시간을
If-Modified-Since헤더의 값과 비교하여:- 리소스가 수정되지 않았다면
304 Not Modified상태 코드로 응답합니다. - 리소스가 수정되었다면 새로운 Last-Modified 헤더와 함께 업데이트된 리소스를 반환합니다.
- 리소스가 수정되지 않았다면
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
10. 요청 제한(Rate limit)
API 과도한 사용을 방지하기 위해 요청 제한을 구현하는 것은 매우 중요합니다. 요청 한도를 초과한 경우 HTTP 상태 코드 429 Too Many Requests가 반환됩니다. 현재는 요청 한도에 도달하기 전에 클라이언트에게 경고를 보내는 표준 방법은 없지만, 다음과 같은 응답 헤더를 통해 관련 정보를 전달하는 것이 일반적입니다:
주요 Rate Limit 헤더
| 헤더 이름 | 설명 | 예시 값 |
|---|---|---|
X-RateLimit-Limit | 현재 기간 동안 허용된 총 요청 수 | 60 |
X-RateLimit-Remaining | 현재 기간 내 남은 요청 수 | 55 |
X-RateLimit-Reset | 현재 기간이 초기화될 때까지의 초 단위 시간 (Unix Timestamp) | 1601299930 |
X-RateLimit-Used | 현재 기간 내 사용된 요청 수 | 5 |
GitHub API 응답 예시
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55
X-RateLimit-Reset: 1601299930
X-RateLimit-Used: 5
Content-Type: application/json
{
"data": { ... }
}
결론
이 글에서 소개한 REST API 디자인 가이드라인을 따르면 보다 일관적이고 사용하기 쉬우며 확장 가능한 API를 구축할 수 있습니다.