HTTP 메서드 스펙상 `POST`는 멱등성을 보장하지 않는다. 하지만 결제나 포인트 충전 같은 민감한 로직에선 네트워크 지연이나 사용자의 실수로 인한 중복 처리를 반드시 막아야 한다. 이를 멱등키를 적용해 구현할 수 있다는 사실을 알게 되었다.

그래서 한번 실험해보려고 한다. 사용자의 포인트를 충전하기 위한 API를 만들어야 하고, 멱등키를 지원해야 한다는 상황이다.
요구사항
- URL: `POST /api/v1/points/charge`
- Header: ` Idempotency-Key`: `String`
- Request Body:
{
"userId": 1,
"amount": 1000
}
- Response Body (성공 시):
{
"status": "SUCCESS",
"currentBalance": 1000,
"message": "충전이 완료되었습니다."
}
설계

Redis에 멱등성 키를 저장한 이유
Redis(캐시)는 보통
- 이 데이터가 얼마나 많이 접근/변경 되는지
- 이 데이터를 생성하는 비용이 얼마나 비싼지
- 다른 사용자들과 얼마나 많이 공유하는지
를 고려하여 적용한다. 위 3가지 상황에 멱등키는 고려될 대상이 아니다. 하지만, 데이터 캐싱이 아닌 분산 락의 역할로 Redis를 선택했다.
비교 항목높은 격리 수준 (가상머신, VM)낮은 격리 수준 (컨테이너)
| 비교 | RDB (MySQL) | Redis (선택됨) |
| 속도 | 느림 (Disk I/O) | 매우 빠름 (Memory) |
| 동시성 제어 | 데드락 위험 | 간단함 (SETNX) |
| 데이터 만료 | 배치 작업 필요 | 자동 (TTL) |
1. DB 보호
트래픽이 몰릴 때 DB 트랜잭션으로만 중복을 막으려면, 서로 Row Lock을 잡으려는 Race Condition이 발생해 데드락이 발생할 수 있다. Redis는 빠른 속도로 요청을 처리하므로, DB 트랜잭션이 시작되기 전에 중복 요청을 사전에 차단하여 DB 자원을 보호할 수 있다.
2. 전역 메모리 역할
단일 서버라면 `ConcurrentHashMap`으로도 해결할 수 있지만, 서버가 여러 대로 스케일 아웃 되는 순간, 각 서버의 메모리는 공유되지 않는다. 따라서 중복 결제 사고가 발생할 수 있다. Redis는 모든 서버가 공통으로 사용하도록 해, 요청이 어떤 서버로 들어오든 일관된 멱등성을 보장할 수 있다.
3. Atomic 연산
Redis는 검사와 저장을 원자적으로 동시에 수행할 수 있다.
4. TTL
멱등성 키는 영구적으로 저장할 필요가 없는데, RDB에 저장한다면 별도의 배치 스케줄러를 돌려 삭제해야 한다. 하지만 Redis는 데이터 저장 시 TTL을 설정하여 알아서 삭제되므로 관리 비용이 작다.
테스트
12개의 스레드에서 동시에 포인트 충전 요청을 보낸다.
예상 결과는
최종 잔액: 1000
히스토리 개수: 1
여야 한다.
결과

배운 점
사실 처음엔 그저, 멱등성 키가 존재하는지만 확인했더니 동작하지 않았다.
// [실패한 코드] 비원자적(Non-Atomic) 로직
if (!redisTemplate.hasKey(key)) { // 1. 확인 (Check)
// --- (이 찰나의 순간에 다른 스레드가 침투!) ---
redisTemplate.opsForValue().set(key, "value"); // 2. 행동 (Act)
return true;
}
return false;

그 이유를 확인해 보니 동시성 환경에서 1번과 2번 사이의 미세한 시간 차이 때문에 중복 체크가 뚫렸다. 이를 해결하기 위해 Redis의 `SETNX(Set If Not Exists) 명령어를 지원하는 `setIfAbsent()`메서드를 적용했다.
// [성공한 코드] 원자적(Atomic) 로직
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, "value", Duration.ofMinutes(10));
이를 통해 확인과 저장을 분리할 수 없는 한 동작으로 수행해 처리할 수 있었다.
물론, 모든 요청이 Redis를 거쳐야 한다는 점에서 병목 지점이 될 수도 있다는 생각이 들었지만, 멱등성 키의 목적은 '비정상적인 중복 처리'를 방지하는 것이며 실제 서비스 환경에서 병목이 발생할 만큼의 대량 중복 요청은 현실적으로 드물다고 판단했다. 오히려 Redis를 통해 DB의 트랜잭션 부하를 막아주는 이점이 훨씬 크다는 결론을 내렸다.
'Network' 카테고리의 다른 글
| [Network] HTTPS는 정말 안전할까? : TLS Handshake의 원리와 PFS (1) | 2025.12.05 |
|---|---|
| [Network] 소켓 통신에서 멀티스레드가 필요한 이유 (0) | 2025.06.26 |
| [Network] HTTP와 HTTPS에 관하여 (0) | 2025.04.17 |
| [Network] TCP 3-way handshake와 4-way handshake (0) | 2025.03.21 |