
이번주 회사에서 고객의 Credit Report을 벤더사에게 요청하는 엔드포인트를 만들어달라는 요청을 받았다. 벤더사에 Credit Report를 처음 요청하면 25불 정도 청구가 되고, 그 이후에 재발급 받으려면 대략 5불정도의 금액이 청구가 된다고 들었다.
이렇게 돈을 다루는 엔드포인트를 만들게 되면, 빠질 수 없는게 멱등성을 가진 엔드포인트를 만드는 일일거다.
멱등성이란?
멱등성은 간단하게 얘기해서 여러번 요청이 들어와도 똑같은 결과를 보여주는 속성을 멱등성이라고 한다. HTTP Request Method에는 멱등성이 있는 친구들이 있고 없는 친구들이 있는데, 요약하면 아래와 같다.
| HTTP Method | 멱등성 여부 |
| GET | O |
| POST | X |
| PUT | O |
| PATCH | X |
| QUERY | O |
| DELETE | O |
| HEAD | O |
POST와 PATCH는 Request를 보냈을 때 결과가 매번 다르게 오기 때문에 (너무 당연한가..?) 멱등성이 없다고 할 수 있다. 내가 이번에 작업한 Endpoint의 Http Request는 POST. 그럼 이 멱등성있는 Endpoint를 만들 때, 어떤 부분을 고려해야 하는지 한 번 알아보자.
재사용성
어떤 코드를 작성할 때마다, 재사용성을 고려하지 않을 수가 없다.
그리고 멱등성을 가진 엔드포인트를 만드는 경우에는 다른 엔드포인트 또한 비슷한 요구사항을 가질 수 있는 경우가 많을 수 있어서, 어노테이션 형식으로 만들기로 했다. 물론 스프링에서 Annotation이지 C#에서는 Attribute이라고 불린다.
[AttributeUsage(AttributeTargets.Method)]
public sealed class IdempotentAttribute: Attribute, IFilterFactory
{
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IserviceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IdempotencyFilter>();
}
}
여기서 중요하게 봐야할건 먼저 Attribute와 AttributeTargets.Method다.
- Attribute를 상속 받음으로서, 이 클래스가 Attribute로서 사용 될 수 있다는걸 선언한다.
- 그리고 AttributeTargets.Method는 Attribute의 사용 범위를 얘기하는데, 우리는 endpoint를 만들 때 method 단위로 만들기 때문에 AttributeTargets.Method를 붙여준다.
멱등한 요청, 상태 그리고 재시도
멱등성은 앞서 말했다시피, 똑같은 결과 값을 줘야한다.
그럼 중복된 요청이 왔을 때, 어떻게 동일한 요청인지 알 수 있을까? IETF는 헤더에 멱등성 키를 포함하는 방법을 제안하고 있다. 헤더에 Idempotency Key 값을 기반으로 서버는 새로운 요청인지, 전에 왔던 요청인지 알 수 있게 된다.
curl https://api.stripe.com/v1/customers \
-u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
-H "Idempotency-Key: KG5LxwFBepaKHyUD" \
-d description="My First Test Customer (created for API docs at https://docs.stripe.com/api)"
그럼 이 멱등성 키를 기반으로, 어떻게 응답을 줘야할까? 그 답은 상태 값을 저장하는거에 따라 다르게 했다.
첫번째 상태 값은 Processing. 이전 요청이 아직 처리 중이어서 응답이 결정되지 않은 상태다. 동일한 멱등성 키로 들어오는 동시 요청을 막기 위해 429 Too Many Requests 에러 코드를 반환하도록 처리했다.
그리고 두번째는 Completed. 유저의 요청이 성공적으로 완료된 상태로 중복 처리가 발생하는 것을 막기 위해, 기존에 저장해 둔 Response Code와 Response Body를 그대로 반환한다.
마지막으로는 Failed. 벤더(Vendor)사의 오류 등으로 인해 처리가 실패한 상태다. 이 경우에는 클라이언트가 재시도(Retry)를 하더라도 허용해주는 케이스다. 물론 Failed 상태로 변경 전에 벤더 사에서 오류가 뜨면, 재시도를 먼저 하고 이 상태 값으로 DB에 저장을 한다.
똑같지만 멱등성 키, 하지만 다른 Body
여러 Edge Case를 고민하다가 이런 의문이 들었다. 만약 클라이언트의 버그로 동일한 멱등성 키를 보냈는데, 정작 요청 Body의 내용이 다르다면 어떻게 처리해야 할까?
이를 해결하기 위해 Request Body를 해싱하여 비교하는 방식을 도입하기로 결정했다. 예를 들어, 동일한 멱등성 키를 가진 요청 A와 B가 있다고 가정해 보자. 두 요청의 Body는 다르지만, 서버가 멱등성 키만 믿고 검증을 넘겨버리면 나중에 온 요청 B는 A의 응답을 돌려받는 정합성 문제가 발생하게 된다.
그래서 상태를 저장할 때 요청 Body를 해싱해서 함께 저장해두는 조건을 추가했다. 이후 중복된 멱등성 키로 요청이 들어오면, 새로 들어온 Body의 해시값과 기존에 저장된 해시값을 비교한다. 만약 두 해시값이 다르다면 페이로드가 조작되었거나 잘못된 요청으로 간주하여 409 Conflict 에러를 반환하도록 방어 로직을 구성했다.
string currentRequestHash = GenerateRequestHash(request);
// DB 또는 Redis에서 기존 요청 기록 조회
var existingRecord = await _idempotencyRepository.GetRecordAsync(idempotencyKey);
if (existingRecord != null)
{
// 키는 같은데 Body Hash가 다르다면 충돌 에러 반환
if (existingRecord.RequestHash != currentRequestHash)
{
return new ObjectResult(new { message = "Idempotency key already used with different payload." })
{
StatusCode = StatusCodes.Status409Conflict
};
}
}
Reference
https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
https://docs.stripe.com/api/idempotent_requests
'백엔드' 카테고리의 다른 글
| gRPC는 언제 써야하는걸까? (0) | 2026.09.13 |
|---|---|
| Controller Layer에 관하여 (0) | 2026.08.02 |
| [Docker] 컨테이너와 이미지 (0) | 2026.07.12 |
| LLM 서버 만들기: Thread와 Process (0) | 2026.05.31 |
| Race Condition과 Deadlock (0) | 2026.05.30 |