Request ID

Q·2024년 12월 17일

개요

  • 만약 결제 시스템의 클라이언트측에서 동일한 request를 중복하여 요청하였을때 어떻게 처리?
  • 이 때 사용할 수 있는 것이 Request_id
  • 네트워크 재전송 시 동일한 Request_id를 유지하여 서버가 중복 요청임을 알 수 있도록 하는 방법은 클라이언트와 서버 간의 협력으로 이루어진다.

1. 클라이언트 측 구현

1-1. Request ID 생성 및 저장

  • 고유한 request_id 생성
    • 요청마다 클라이언트가 고유한 request_id를 생성
    • 고유성을 보장하기 위해 UUID, ULID, 또는 타임스탬프 기반의 고유 식별자를 사용
import uuid
request_id = str(uuid.uuid4())
  • request_id를 요청과 함께 저장
    • 요청이 성공적으로 처리될 때까지 동일한 request_id를 재사용
    • 요청의 재전송이 필요할 경우, 이전에 생성한 request_id를 재사용

1-2. 요청 시 Request ID를 포함

  • 클라이언트는 요청을 전송할 때 HTTP 헤더나 쿼리 파라미터에 request_id를 포함

  • 헤더 예시
POST /api/v1/resource HTTP/1.1
Host: example.com
X-Request-ID: 123e4567-e89b-12d3-a456-426614174000
  • 쿼리 파라미터 예시
POST /api/v1/resource?request_id=123e4567-e89b-12d3-a456-426614174000 HTTP/1.1

1-3. 요청 재전송 시 동일 Request ID 사용

  • 네트워크 재전송(예: 클라이언트에서 타임아웃 발생) 시 동일한 request_id를 재사용
    • HTTP 클라이언트 라이브러리(예: requests, axios)에서 타임아웃 및 재시도 로직을 설정하고, 동일한 request_id를 재사용
import requests

headers = {"X-Request-ID": request_id}
try:
    response = requests.post("https://example.com/api/v1/resource", headers=headers, timeout=5)
except requests.exceptions.Timeout:
    response = requests.post("https://example.com/api/v1/resource", headers=headers, timeout=5)

1-4. 상태 저장 (Optional)

  • 클라이언트가 요청 상태를 관리할 수 있다면, request_id와 해당 요청의 결과 상태를 로컬 캐시에 저장하여 중복 전송을 방지

2. 서버 측 구현

2-1. Request ID 기록

  • 서버는 수신된 request_id를 기록하여, 동일한 요청이 다시 들어올 경우 중복 요청인지 식별
  • 기록 방식
  1. Database
    request_id를 데이터베이스의 테이블에 저장.
CREATE TABLE processed_requests (
    request_id VARCHAR(36) PRIMARY KEY,
    status VARCHAR(20),
    response TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  1. Cache
    Redis와 같은 캐시를 사용하여 request_id를 저장하고, 일정 시간(TTL) 동안 유지.

2-2. 중복 요청 감지

  • 서버는 요청을 처리하기 전에 request_id가 이미 기록되어 있는지 확인
    • 기록되어 있다면 중복 요청으로 간주하고, 기존 응답을 반환하거나 요청을 무시
    • 기록되지 않았다면 요청을 처리하고, request_id와 결과 상태를 저장
  • Database 기반 예시
def handle_request(request_id, request_data):
    # Check if the request_id is already processed
    existing_request = db.get("SELECT * FROM processed_requests WHERE request_id = ?", (request_id,))
    if existing_request:
        return existing_request["response"]  # Return cached response

    # Process the new request
    response = process_request(request_data)
    db.execute("INSERT INTO processed_requests (request_id, status, response) VALUES (?, ?, ?)",
               (request_id, "processed", response))
    return response
  • Redis Cache 기반 예시
import redis

cache = redis.StrictRedis(host='localhost', port=6379, db=0)

def handle_request(request_id, request_data):
    if cache.get(request_id):
        return "Duplicate request: already processed"

    # Process the new request
    response = process_request(request_data)
    cache.set(request_id, "processed", ex=3600)  # Cache for 1 hour
    return response

2-3. Idempotent Endpoint 설계

  • 서버의 엔드포인트를 Idempotent하게 설계하여, 동일한 요청이 여러 번 처리되더라도 결과가 동일하도록 보장합니다.
    • 예: 리소스 생성 요청에서 request_id 기반으로 리소스의 존재 여부를 확인.
def create_resource(request_id, resource_data):
    existing_resource = db.get("SELECT * FROM resources WHERE request_id = ?", (request_id,))
    if existing_resource:
        return existing_resource  # Return existing resource

    # Create new resource
    new_resource = create_new_resource(resource_data)
    db.execute("INSERT INTO resources (request_id, data) VALUES (?, ?)", (request_id, new_resource))
    return new_resource

3. 네트워크 재전송과 Request ID 관리 전략

  1. 클라이언트 측
  • 고유한 request_id를 생성하여 각 요청에 포함.
  • 요청 재전송 시 동일한 request_id를 재사용.
  1. 서버 측
  • request_id를 기록하여 중복 요청 감지.
  • 이미 처리된 요청은 결과를 반환하거나 무시.
  1. 추적 및 로깅
  • 모든 요청과 request_id를 로깅하여, 요청 흐름을 추적 가능하게 설정.

4. 예제 시나리오

클라이언트 요청

POST /api/v1/resource HTTP/1.1
Host: example.com
X-Request-ID: 123e4567-e89b-12d3-a456-426614174000

서버 동작
1. 123e4567-e89b-12d3-a456-426614174000이 이미 기록되어 있는지 확인
2. 기록되어 있으면

  • 이전 결과를 반환
{
    "status": "success",
    "message": "Request already processed",
    "data": {...}
}
  1. 기록되어 있지 않으면
  • 요청 처리 후 request_id와 결과를 저장.
profile
Data Engineer

0개의 댓글