멱등성과 버전 관리가 적용된 REST API 설계 방법
새로운 기능을 배포했는데, 갑자기 API가 타임아웃 후 재시도하는 클라이언트로부터 중복 POST 요청을 받기 시작합니다. 데이터베이스에는 이제 동일한 주문이 두 개 있습니다. 또는 호환성을 깨뜨리는 변경을 배포했는데, 모바일 앱이 이전 응답 형식을 기대하다가 충돌합니다. 이는 API 설계에서 가장 흔하고 고통스러운 두 가지 문제입니다: 비멱등성 작업과 호환성 깨짐입니다. 이 가이드는 REST API의 멱등성과 버전 관리를 위한 실용적인 패턴으로 두 문제를 모두 해결하는 방법을 보여줍니다.
REST API에서 멱등성이 중요한 이유
멱등성이란 동일한 요청을 여러 번 해도 한 번 한 것과 같은 효과를 갖는 것을 의미합니다. HTTP 메서드는 정의된 멱등성 의미를 가집니다: GET, HEAD, PUT, DELETE, OPTIONS는 멱등성이 있고, POST와 PATCH는 그렇지 않습니다. 그러나 실제 API에서는 리소스 생성이나 결제 처리 같은 작업에 POST를 받아들여야 하는 경우가 많습니다. 네트워크 장애, 클라이언트 타임아웃, 재시도 로직은 중복을 일으킬 수 있습니다. 멱등성이 없으면 이중 청구, 중복 레코드, 일관성 없는 상태의 위험이 있습니다.
멱등성 REST API를 위한 기법
1. POST 요청에 멱등성 키 사용
가장 일반적인 접근 방식은 클라이언트가 Idempotency-Key 헤더에 고유 키(예: UUID)를 보내도록 하는 것입니다. 서버는 키와 작업 결과를 저장합니다. 동일한 키가 다시 수신되면 서버는 작업을 재실행하지 않고 저장된 결과를 반환합니다.
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
구현 단계:
- 클라이언트는 각 논리적 작업에 대해 고유 키(UUID v4)를 생성합니다.
- 서버는 영구 저장소(예: Redis 또는 데이터베이스)에 키가 존재하는지 확인합니다.
- 없으면 요청을 처리하고 응답 상태와 본문과 함께 키를 저장합니다.
- 있으면 재처리 없이 저장된 응답을 반환합니다.
무한한 저장소 증가를 피하기 위해 합리적인 만료 시간(예: 24시간)을 설정하세요.
2. 조건부 요청 활용
업데이트의 경우 ETag와 If-Match 헤더를 사용하여 업데이트 손실을 방지합니다. 서버는 현재 상태를 나타내는 ETag를 반환합니다. 클라이언트는 If-Match와 함께 이를 다시 보냅니다. 리소스가 변경되었으면 서버는 412 Precondition Failed를 반환합니다. 이는 PUT 요청을 경쟁 조건으로부터 안전하게 만듭니다.
PUT /articles/123
If-Match: "abc123"
Content-Type: application/json
{"title": "Updated title"}
3. 자연스러운 멱등성을 위해 PUT 설계
클라이언트가 리소스 URL을 결정할 수 있을 때는 POST보다 PUT을 선호하세요. 예를 들어 PUT /users/{userId}는 반복 호출이 동일한 리소스를 덮어쓰기 때문에 자연스럽게 멱등성이 있습니다. 서버가 ID를 할당할 때만 POST를 사용하세요.
4. 고유 제약 조건으로 중복 감지 처리
멱등성 키를 비즈니스 키(예: 주문 번호)에 대한 데이터베이스 고유 제약 조건과 결합하세요. 중복이 통과하면 데이터베이스가 이를 거부하고, 명확한 메시지와 함께 409 Conflict를 반환할 수 있습니다.
REST API의 버전 관리 전략
API는 진화합니다. 새로운 필드, 변경된 동작, 제거된 엔드포인트는 기존 클라이언트를 깨뜨릴 수 있습니다. 버전 관리를 통해 클라이언트를 방해하지 않고 변경 사항을 도입할 수 있습니다. 몇 가지 일반적인 전략이 있습니다:
| 전략 | 예시 | 장점 | 단점 |
|---|---|---|---|
| URI 경로 | /v1/users |
명시적, 라우팅 쉬움, 캐시 친화적 | URL 변경, 복잡해질 수 있음 |
| 쿼리 매개변수 | /users?version=1 |
간단, 선택적 | 누락 쉬움, RESTful하지 않음 |
| 사용자 정의 헤더 | Accept-Version: v1 |
URL 깔끔하게 유지 | 브라우저에서 테스트 어려움 |
| 미디어 타입 | Accept: application/vnd.api.v1+json |
콘텐츠 협상, 순수 REST | 복잡, 덜 일반적 |
대부분의 팀에게 URI 경로 버전 관리가 가장 실용적인 선택입니다. 가시적이고, 문서화하기 쉬우며, API 게이트웨이와 CDN과 잘 작동합니다.
클라이언트를 깨뜨리지 않고 버전 관리하는 방법
- 처음부터 경로에 v1로 시작하세요. 나중에 버전 관리를 추가하는 것은 더 어렵습니다.
- 버전 내에서는 추가적 변경만 하세요: 새로운 선택적 필드, 새로운 엔드포인트. 필드를 제거하거나 이름을 바꾸지 마세요.
- 우아하게 폐기하세요: 지원 종료를 알리고, 마이그레이션 가이드를 제공하며, 이전 버전의 사용을 모니터링하세요.
- Sunset 헤더 사용:
Sunset: Sat, 31 Dec 2025 23:59:59 GMT로 클라이언트에 알리세요. - 유지 관리 부담을 제한하기 위해 최대 두 개의 활성 버전만 유지하세요.
종합하기: 실용적인 예시
결제 API를 고려해 보세요. 결제를 멱등적으로 생성하고 엔드포인트를 버전 관리하려고 합니다.
POST /v1/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
서버 로직:
- Redis에
Idempotency-Key가 존재하는지 확인합니다. - 있으면 캐시된 응답(상태 + 본문)을 반환합니다.
- 없으면 결제를 처리하고, 응답과 함께 키를 저장하고, 201 Created를 반환합니다.
업데이트의 경우 ETag와 함께 PUT을 사용하세요:
PUT /v1/payments/123
If-Match: "xyz789"
Content-Type: application/json
{"amount": 1500}
ETag가 일치하지 않으면 412를 반환합니다. 이는 동시 변경 사항을 덮어쓰는 것을 방지합니다.
일반적인 함정과 피하는 방법
- 멱등성 키를 영구 저장: 저장소 비대화를 피하기 위해 TTL(예: 24시간)을 설정하세요.
- 경쟁 조건 무시: 키를 확인하고 설정하기 위해 원자적 작업(예: Redis SETNX)을 사용하세요.
- 너무 늦은 버전 관리: 처음부터 /v1을 추가하세요.
- 마이너 버전의 호환성 깨짐: 응답 구조를 변경하는 모든 변경을 메이저로 취급하세요.
- 멱등성 문서화 안 함: 어떤 엔드포인트가 Idempotency-Key를 지원하는지 명확히 명시하세요.
FAQ
멱등성 REST API란 무엇인가요?
멱등성 REST API는 동일한 요청을 여러 번 해도 한 번 한 것과 같은 결과를 생성하도록 보장합니다. 이는 특히 POST와 같은 비멱등성 메서드의 재시도를 안전하게 처리하는 데 중요합니다.
어떤 HTTP 메서드가 멱등성이 있나요?
GET, HEAD, PUT, DELETE, OPTIONS, TRACE는 멱등성이 있습니다. POST와 PATCH는 기본적으로 멱등성이 없지만, 멱등성 키 같은 기법을 사용하여 멱등성 있게 만들 수 있습니다.
REST API를 버전 관리하는 가장 좋은 방법은 무엇인가요?
URI 경로 버전 관리(예: /v1/users)가 가장 일반적이고 실용적인 접근 방식입니다. 명시적이고, 라우팅하기 쉬우며, 캐싱과 API 게이트웨이와 잘 작동합니다. 다른 옵션으로는 쿼리 매개변수, 사용자 정의 헤더, 미디어 타입이 있지만 각각 장단점이 있습니다.
API의 JSON 응답을 테스트할 준비가 되셨나요? JSON Formatter를 사용하여 페이로드를 빠르게 검증하고 예쁘게 출력하세요.