리팩토링 개요 및 상황
(문제의 시작) 내부 결제 수단(Pay)를 먼저 구현하면서 Pay가 결제 요청의 진입점이자 오케스트레이터라고 판단
-> Pay가 결제 요청 수신, 흐름 조율 담당
(발생한 의문) 그런데 Pay가 금전 원장 관리하면서 결제 확정까지 직접 수행하는 건 책임 분리에 맞지 않다고 판단
-> 결제 성공 판단을 별도의 Payment 서비스로 분리
1) Pay를 도메인 기준으로 다시 분리해보니 기존 구조에서 PG + Pay 역할이 명확히 분리되지 않았음
2) 도메인 별로 분산 락 적용했지만, 락 기준이 요청 단위가 아닌 서비스 단위로 분산 -> 동일 자원에 대한 동시성 제어 일관 X
= Pay, Transaction, Payment 각각에서 서로 다른 락 사용
-> 동일 유저의 결제 요청이라도 서로 다른 락 영역에서 동시에 처리될 수 있었음
*락이 같다 = 같은 key를 기준으로 같은 저장소에서 확인되는지
-> 각 서비스가 `userId`로 락을 걸고 있지만 실제로는 서로 완전히 다른 key
<상황>
유저 1
Pod A → Pay 락 획득 (pay:1)
Pod B → Transaction 락 획득 (transaction:1)
Pod C → Payment 락 획득 (payment:1)
=> 동시에 실행 -> 동시성 제어 실패
해결 1) 서비스 구조 변경
변경 전
Pay(Orchestrator, Ledger) -> Transaction -> Payment
*MSA 환경에서 결제 시스템 구축하기 1 - 서비스 개요 & 책임 분리 기반 상태 전이 결제 설계 참고
[PlantiFy/v2] MSA 환경에서 결제 시스템 구축하기 1 - 서비스 개요 / 책임 분리 기반 상태 전이 결제
결제 서비스 개요자체 결제 수단을 운영하고, 이를 기반으로 결제, 환불, 정산까지 처리하는 전용 페이먼트 플랫폼외부 PG나 카드망을 거치지 않고, 내부 원장과 트랜잭션 상태를 직접 관리 단일
debug.tistory.com
변경 후
- Payment(Orchestrator): PG 진입점 + 결제 흐름 조율 단일 책임
- Pay(Ledger): 금전 원장성 변경 담당
- 잔액 차감/복원, 포인트 사용/적립, 정산 기록
- Transaction: 결제 결과를 최종 상태로 확정
- 최종 성공 상태는 금전 변경 이후에만 전이되도록 보장
Payment(Orchestrator) -> Transaction(PENDING) -> Pay(Ledger) -> Transaction(COMPLETED)
해결 2) 락 범위를 Ledger로 한정
변경 전
*MSA 환경에서 결제 시스템 구축하기 2 - Redis 분산 락과 멱등성 설계로 동시성 리팩토링 참고
[PlantiFy/v2] MSA 환경에서 결제 시스템 구축하기 2 - Redis 분산 락과 멱등성 설계로 동시성 리팩토링
결제 시스템에서 가장 어려운 부분은 결제 확정(Confirm Payment) 단계Pay 잔액 차감 / 포인트 사용 or 적립 / 트랜잭션 상태 확정 / 정산 데이터 기록 등 여러 상태가 동시에 변경됨IF. MSA 환경에서 네트
debug.tistory.com
변경 후
Payment (Orchestrator)
돈을 직접 건드리지 않고 결제 흐름 조율, Payment 상태 관리, 승인 이벤트 발행 담당
- 결제 진입 중복 방지: `orderId` unique
- Ledger 중복 차감 방지: `transactionId` unique / exists check
- Payment-Transaction 연결 보장: `transactionId` unique
Pay (Leger)
`userId` 기반 Redis 분산 락 (Redisson / RedLock)
같은 유저의 돈 변경 반드시 직렬화
다중 Pod에서도 안전
<`transactionId`가 아닌 `userId`로 락을 잡는 이유>
락 기준: 자원(resources) 기준
- `transactionId`: 요청 단위
- `userId`: 공유 자원 (잔액)
-> 현재 보호해야 하는 건 잔액(balance)
=> 유저가 결제 중에 또 다른 결제를 시도하면 잔액 문제가 생길 수 있음
<상황1: `transactionId` 기준>
결제 A (`transactionId`=1) -> 1000원 차감
결제 B (`transactionId`=2) -> 1000원 차감
잔액 = 1500원
-> 서로 다른 `transactionId`는 동시에 실행됨
=> 1500 - 1000 - 1000 = -500 (X)
<상황2: `userId` 기준>
`userId`=1
A 처리 중 -> 락 점유
B 요청 -> 대기
-> 순차 처리됨
=> 1500 - 1000 = 500 -> 500 - 1000 = 실패 or 차단 (O)
=> `transactionId`: 재시도/중복 호출 방지, `userId`: 동시에 실행 방지
Transaction
돈을 직접 건드리지 않고 상태만 변경 -> 락 없음
참고
RedLock은 안전하지 않다?
Martin Kleppmann (비판)
- TTL 기반 락은 깨질 수 있다
: GC pause / 네트워크 지연 -> 락 만료됐는데도 작업 계속할 수 있음 - 시간에 의존하는 설계 자체가 위험하다
: clock drift, 네트워크 지연 -> mutual exclusion 깨질 수 있음
How to do distributed locking — Martin Kleppmann’s blog
How to do distributed locking Published by Martin Kleppmann on 08 Feb 2016. As part of the research for my book, I came across an algorithm called Redlock on the Redis website. The algorithm claims to implement fault-tolerant distributed locks (or rather,
martin.kleppmann.com
antirez (반박)
- TTL 없는 락은 더 위험하다
: 프로세스 죽으면 영원히 락 유지됨 -> deadlock => auto-release 필수 - fencing token 써야 더 안전하다고 했는데 그럼 애초에 lock을 쓸 필요 없음
= 이미 데이터 레이어에서 race 해결 가능하면 락 자체가 필요 없는 상황 - 시간 가정 monotonic clock 쓰면 충분하다
- 실제로 문제 사례 없다
Is Redlock safe? - <antirez>
antirez.com
'💻 > 프로젝트' 카테고리의 다른 글
| [PlantiFy] MSA 환경에서 JWKS 기반 분산 인증 구현 - RS256 전환 + common-auth-lib (0) | 2026.06.01 |
|---|---|
| [PlantiFy] 숲 꾸미기 & 캐시 서비스 - 아이템 구매 시 캐시 차감 동시성 처리 (0) | 2025.11.28 |
| [PlantiFy] 숲 꾸미기 서비스 - UI 행위 단위 GraphQL 설계 (0) | 2025.11.28 |
| [PlantiFy/v2] MSA 환경에서 결제 시스템 구축하기 2 - Redis 분산 락과 멱등성 설계로 동시성 리팩토링 (0) | 2025.11.21 |
| [PlantiFy] MSA 환경에서 Kakao Social Login 구현하기 - 서비스 개요 / Auth 병목 문제와 JWKS 기반 분산 인증 구조 개선 (0) | 2025.11.14 |