01
멀티 벤더 결제 시스템 및 미수 복구 안정화
서비스 초기 설계부터 구현까지 전담
결제 서비스 초기 설계부터 구현까지 전담하며 단일 결제 구조를 멀티 벤더 모듈로 확장하고, DLQ 기반 미수 처리와 DB 승인권 기반 중복 결제 방지 체계를 구축했습니다. READY 고아 주문과 AUTHORIZED 체류 건을 자동 복구·수동 판단 경로로 연결해 결과 미확정 상태를 추적 가능하게 만들었습니다.
설계 배경
복수 벤더를 같은 확장 지점으로 수용하는 것뿐 아니라, Redis 락 만료·PG 응답 유실·DB 롤백·벤더 변경이 겹쳐도 같은 주문이 재승인되지 않는 최종 안전장치와 복구 경계가 필요했습니다.
강조 포인트
멀티 PG 확장성뿐 아니라 결제 상태머신, 원자적 승인권, 결과 미확정 복구 경계를 함께 설명할 수 있는 프로젝트입니다.
핵심 구현
- 추상 클래스 기반 벤더 전략 패턴으로 PG사 통합 아키텍처를 구성했습니다.
- GCP Pub/Sub 기반 DLQ 패턴을 구현하고 실패 이벤트를 별도 큐로 격리해 미수 처리 대상이 추적 가능하도록 구성했습니다.
- PG 호출 직전 `payments.status`를 `AUTHORIZED`로 조건부 전이하는 DB 승인권을 두어, 상태 저장소가 한 주문의 승인 주체를 원자적으로 선점하도록 설계했습니다.
- 결제 확정 트랜잭션에서 미수 부기를 분리해 부기 락 경합이 승인 성공을 되감지 않게 하고, 미수 재결제에서 포인트가 이중 차감되거나 현재 설정이 과거 주문에 소급되던 경로를 교정했습니다.
- READY 고아 주문의 상태 동기화·이벤트 재발행과 AUTHORIZED 체류 해소 배치·수동 복구 API를 구성해 자동 복구와 운영 확인 경계를 분리했습니다.
엔지니어링 관점
- 결제 요청, 포인트 hold/confirm, 성공·실패 업데이트를 PaymentProcessor 중심으로 묶어 상태 전이를 한 곳에서 추적할 수 있게 설계했습니다.
- Redis 락은 요청 동시성을 줄이는 1차 장치로 두고, 돈이 움직이기 직전에는 DB 조건부 UPDATE가 최종 직렬화를 보장하도록 방어선을 분리했습니다.
- 승인 결과가 명확하면 PAID/FAILED로 전이하고 결과가 불확실하면 AUTHORIZED에 남기는 fail-closed 모델로, 가용성보다 이중 청구 방지를 우선했습니다.
- 동일 주문을 8개 스레드로 동시에 선점하는 실 MySQL 테스트를 두어 정확히 1건만 승인권을 얻는지 검증하고, 목 테스트가 놓치는 조건부 UPDATE의 직렬화 보장을 회귀 테스트로 고정했습니다.
Mermaid로 보는 핵심 구조
Mermaid View
멀티 벤더 결제 오케스트레이션: 락, hold, 상태 전이
PaymentProcessor 기준으로, 요청 데이터와 락 키, 포인트 hold, payment READY, `PG Vendor` 내부 승인 단계, 성공/실패 상태 전이가 한 장 안에서 자연스럽게 이어지도록 정리했습니다.
flowchart TD
Req["pay / rePay<br/>order=ORD-240915-001<br/>user=421 method=17 point=2000"] --> Lock["distributed lock<br/>payment-user-process:421"]
Lock --> Hold["PointUpdater.hold()<br/>wallet -> HOLD 2000P"]
Hold --> Ready["createWithReady()<br/>payment READY"]
Ready --> Sub["resolve subscription<br/>methodId=17 or primary"]
Sub --> Vendor["Payment Provider Router<br/>Kakao Pay / Toss Payments / Kakao T<br/>selected: Kakao T"]
subgraph PGV["Payment Provider Layer"]
direction TD
Vendor --> Keys["read vendor keys<br/>pgPayKey + token"]
Keys --> Api["vendor client.pay(...)"]
Api --> Tx["save pgTransactionId<br/>paymentId / tid / paymentKey"]
end
Tx --> Result{"approval result"}
Result -->|success| Done["updateSuccess<br/>payment PAID<br/>point HOLD->CONFIRM"]
Result -->|fail| Fail["updateFailed<br/>releaseHold(order)"]
Fail --> Recovery["repair / retry / failover"]
classDef vendor fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
class Vendor,Keys,Api,Tx vendor;
style PGV fill:#eef7fb,stroke:#0f4c81,stroke-width:2px,color:#0f172a; Mermaid View
멀티 Vendor 시스템: 필수 계약과 선택 확장
`VendorType` 확장 지점 위에 `VendorChecker.select()`를 두고, 모든 벤더에 필요한 계약과 특정 벤더에만 필요한 기능을 분리했습니다. `@RequiredVendor`와 `VendorRequirementsValidator`가 앱 시작 시 필수 구현 누락을 막고, `VendorPaymentOnceProcessor(KakaoPay)`나 repair 서비스는 필요한 벤더에만 붙도록 구성했습니다.
flowchart TD
Vendors["VendorType<br/>Kakao Pay / Toss Payments / Kakao T"] --> Select["VendorChecker.select(vendorType)"]
Select --> Required["Required on all vendors<br/>VendorPaymentProcessor<br/>VendorMethodProcessor"]
Select --> Partial["Required on some vendors<br/>VendorPaymentOnceProcessor<br/>(KakaoPay only)"]
Select --> Optional["Optional extensions<br/>RepairService / vendor hooks"]
Required --> Validate["@RequiredVendor<br/>+ VendorRequirementsValidator"]
Partial --> Validate
Validate --> Boot{"startup validation"}
Boot -->|missing| Error["application start fail"]
Boot -->|ok| Route["route to concrete impl"]
classDef core fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
classDef optional fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
classDef error fill:#fff1f2,stroke:#be123c,stroke-width:2px,color:#0f172a;
class Vendors,Select,Required,Partial,Validate,Route core;
class Optional optional;
class Error error; 운영 결과
- 복수 PG를 단일 인터페이스로 통합하고, 새로운 벤더가 추가돼도 같은 구조로 확장 가능한 결제 아키텍처를 만들었습니다.
- DLQ 기반 미수 처리와 READY/AUTHORIZED 상태별 복구 경로를 운영에 적용해 실패 지점을 자동 복구 또는 수동 판단 가능한 상태로 표면화했습니다.
- 승인 성공 후 DB 경합·응답 유실·다른 PG로의 재시도가 이중 청구로 번지는 경로를 상태 기반으로 차단했습니다.