꾸준하게
결제 시에 멱등성 보장 및 결제 불일치 상황 해결 본문
결제 시스템에서 가장 중요한 것은 돈은 정확하게 한 번만 나가야 한다는 것 입니다.
네트워크 불안정이나 사용자의 실수로 인해 동일한 결제 요청이 중복으로 들어오더라도, 서버는 항상 동일한 응답을 내려주어야 합니다.
이를 멱등성이라고 합니다.
저는 이를 보장하기 위해 Redis 분산 락과 캐싱 전략을 조합하였습니다.

요청이 들어오면 먼저 Redis에 해당 멱등 키로 락을 획득하려고 시도합니다. Redis의 SET NX EX 40 명령어를 사용하는데, 이는 "키가 없을 때만 값을 설정하고, 20초 후 자동 만료"를 원자적으로 수행합니다. 이 시점에 캐시가 있다면 이미 다른 요청이 처리를 완료한 것이므로, 락을 해제하고 캐시된 결과를 그대로 반환합니다. 캐시가 없다면 실제 결제 검증 로직을 수행하고, 완료 후 결과를 Redis에 24시간 TTL로 캐싱합니다. 마지막으로 락을 해제하여 대기 중인 요청들이 캐시된 결과를 읽어갈 수 있도록 합니다.

락 TTL을 40초로 설정한 이유는 대부분 PG사 가이드에서 Receive(Read) timeout을 30초로 제시하고 있습니다. Read-timeout 발생 시 망취소 요청을 통해 결제 정보 불일치를 방지하도록 안내하고 있어, 승인 요청의 타임아웃(30초)과 타임아웃 처리(망취소/상태확인)까지 포함한 부가 처리 시간을 고려해 10초의 버퍼를 추가했습니다.
결제 승인 처리 시 멱등성 보장
유저가 PG사와 통신 후 결제 처리를 요청하는 과정이 있습니다.
이 때 중복 요청이 올 수 있는 경우가 있고 이를 해결하기 위해 유저가 결제 승인 요청을 한 후에 일정 시간 동안 멱등성을 보장해야 합니다.
// 2. 이미 처리된 요청인지 확인
Optional<String> cachedResult = idempotencyService.getIfProcessed(idempotencyKey);
if (cachedResult.isPresent()) {
log.info("Returning cached result for idempotency key: {}", idempotencyKey);
return ResponseEntity.ok(cachedResult.get()); // 캐시된 결과 반환
}
// 3. 처리 중으로 마킹 (동시 요청 방지)
if (!idempotencyService.markAsProcessing(idempotencyKey)) {
return ResponseEntity.status(HttpStatus.CONFLICT) // 409 에러 반환
.body("Request is already being processed. Please wait.");
}
토스페이먼츠 기술블로그 참고 : https://docs.tosspayments.com/blog/what-is-idempotency
409 에러 반환을 안한다면 어떻게 될까
만약 돈이 오가는 결제 요청에서 사용자가 '따닥' 더블 클릭을 했다면, 두 번째 요청은 409에러를 즉시 반환해서 사용자와 서버측 모두 효율적으로 대응할 수 있습니다.
409 반환이 없다면 같은 키 요청을 대기시키게 됩니다. 이는 클라이언트가 또 재시도 발생할 수 있게 되고 중복 요청이 계속해서 쌓이게 되어 워커 스레드가 고갈되는 현상이 일어날 수 있습니다.
타임아웃으로 인한 서버와 PG사 결제 불일치 상황
결제 승인요청을 했을 때 응답이 없어 Read-timeout이 발생하는 경우가 생길 수 있습니다.

위 문의처럼 서버의 timeout이 일어났지만 실제로는 PG사에서 정상적으로 결제가 승인되었을 수도 있습니다.
(Toos Payments 결제승인 API timeout 관련 문의 : https://techchat.tosspayments.com/m/1261254382864039996)
우선적으로 PG사에서 제공하는 가이드라인에 맞추어 서버 Read timeout을 설정하는 것이 중요합니다.

나이스 페이먼츠 Github : https://github.com/nicepayments/nicepay-manual/blob/main/common/preparations.md
나이스 페이먼츠는 바로 망취소를 하라는 가이드라인을 제공하고 있습니다.
이것보다는 저는 결제 상태 조회 API를 통해 상태값에 따라 케이스를 분류하는 방식으로 보완하면 어떨까 생각하였습니다.


이렇게 하면 더 안전하게 상태에 따라 처리할 수 있다는 논리적 장점이 있습니다.
하지만 막상 생각해보면 조회 API 호출 없이도 nicepayments 처럼 취소 API를 호출해도
- 존재 하지 않은 결제이면 404 상태값 반환
- 만약 있었다면 결제 취소 완료 반환
이렇게 되기에 굳이 한번 더 조회 API를 호출하지 않아도 되겠다는 생각을 했습니다. 그래서 nicepayments의 가이드라인에 맞추어 설계하였습니다.
'테크 > 개발' 카테고리의 다른 글
| 재고 관리 순서에 따른 동시성 제어 전략 (0) | 2026.02.06 |
|---|---|
| @Transactional은 어떻게 동작하는걸까? (0) | 2025.07.01 |
| 연관관계 조인 전략에 대하여 (0) | 2024.03.07 |
| [자바 ORM 표준 JPA 프로그래밍] 영속성 관리 (0) | 2023.09.10 |
| [Spring Boot] AccessToken, RefreshToken을 이용한 로그인 구현 (0) | 2023.07.22 |
