결제 생성부터 PSP(토스페이먼츠) 승인, 정산, 복식부기 원장 기록, 복구까지 전체 흐름을 다루는 결제 시스템. 인프런 「견고한 결제 시스템 구축」 강의를 기반으로 학습하며 Kotlin + Kafka + Hexagonal Architecture 3개 마이크로서비스로 구현.
발행 원자성, 중복 소비 방지, 미확정 상태 복구까지 — 결제 시스템의 신뢰성 과제 해결
결제 주문 생성부터 토스페이먼츠 API 승인까지. 타임아웃(connect 3s / read 10s)과 실패 상태 관리로 외부 PSP 연동의 불확실성을 다룸.
승인 성공 후 Kafka에 바로 발행하지 않고 같은 트랜잭션에서 outbox에 저장 → relay가 발행. DB 커밋과 이벤트 발행의 원자성 보장.
wallet·ledger 서비스는 processed-message 테이블로 중복 메시지를 방지. Kafka의 at-least-once 전달에서도 정확히 한 번 처리 효과.
판매자 지갑 정산에 @Version 낙관적 락 + 재시도로 동시성 충돌 완화. ledger 서비스는 복식부기로 원장 기록.
세 서비스 모두 재시도 불가능한 실패를 .dlt 토픽으로 격리. 지수 백오프 + 지터 재시도 정책을 서비스별로 설정.
EXECUTING·UNKNOWN 상태의 결제를 주기적으로 스캔해 복구. PSP 응답 유실 등 "결과를 모르는" 결제를 방치하지 않는 설계.
payment-service(체크아웃·승인·outbox relay·복구) → payment.confirmed 이벤트 → wallet-service(정산)·ledger-service(원장)가 병렬 처리 → 완료 이벤트를 모아 payment-service가 최종 완료 처리.
checkout → confirm → outbox → kafka → wallet/ledger → completedomain(프레임워크 무의존) / application(포트) / adapter.in(HTTP·Kafka·스케줄러) / adapter.out(DB·Kafka·Toss API) / bootstrap(wiring). 세 서비스 모두 동일 기준 적용.
domain / application / adapter.in / adapter.out / bootstrapOutbox(발행 원자성) + 멱등 소비(중복 방지) + 낙관적 락(동시성) + DLQ(실패 격리) + Recovery Scheduler(미확정 상태 복구)를 결제 흐름 각 지점에 배치해 장애 시나리오별 대응.
outbox · idempotent consume · optimistic lock · DLQ · recoveryDB 커밋 후 Kafka 발행 사이에 프로세스가 죽으면 이벤트 유실, 발행 후 커밋 실패면 유령 이벤트. 같은 트랜잭션에서 outbox에 저장하고 relay가 발행해 두 실패 모드를 모두 제거.
Kafka는 at-least-once — 재시도·리밸런싱으로 중복 전달이 정상 동작. 컨슈머 쪽 processed-message 테이블로 "정확히 한 번 처리"를 소비 측에서 보장하는 것이 현실적.
정산은 충돌 빈도가 낮은 워크로드 — 비관적 락으로 커넥션을 점유하기보다 @Version + 재시도가 처리량에 유리. 충돌 시 재시도로 수렴.
PSP 호출 타임아웃 시 결제는 성공도 실패도 아닌 UNKNOWN. 이를 방치하면 돈과 상태가 어긋남 — 주기 스캔으로 PSP에 재조회해 상태를 수렴시키는 복구 경로를 상시 운영.