여러 PG사를 통합 연동하는 결제 오케스트레이션 시스템. 가맹점은 하나의 API만 연동하면 토스페이먼츠, KG이니시스 등 다양한 PG를 사용할 수 있다.
| 구분 | 기술 |
|---|---|
| Language | Kotlin |
| Framework | Spring Boot 3.5.0 |
| DB | MySQL 8.0 |
| Cache | Redis |
| Migration | Flyway |
| Build | Gradle (Kotlin DSL) |
| JDK | 21 |
| Docs | Swagger (springdoc-openapi) |
| Resilience | Resilience4j |
가맹점 → Payments (결제 오케스트레이터) → PG사 → 카드사
payments/
├── payment/ # 결제 핵심
│ ├── controller/ # REST API
│ ├── service/ # 결제 승인/취소 로직
│ ├── domain/ # Payment 엔티티, PaymentStatus 상태머신
│ ├── dto/ # Request/Response DTO
│ └── repository/ # JPA Repository
├── pg/ # PG 연동
│ ├── connector/ # PG 커넥터 인터페이스 (추상화)
│ ├── router/ # PG 라우팅 (장애 시 fallback)
│ └── mock/ # 테스트용 Mock PG
├── stock/ # 재고 동시성 제어 (보강)
│ ├── controller/ # 락 전략별 주문 API
│ ├── service/ # 비관적/낙관적/분산 락/원자적 UPDATE
│ ├── domain/ # Product, StockOrder 엔티티
│ ├── dto/ # Request/Response DTO
│ └── repository/ # SELECT FOR UPDATE, 원자적 UPDATE 쿼리
├── shipping/ # 배송 API 연동 + 장애 격리 (보강)
│ ├── connector/ # ShippingConnector 인터페이스 + CircuitBreaker 데코레이터
│ ├── router/ # 배송사 라우팅 (fallback)
│ ├── mock/ # Mock 배송사 A, B
│ ├── config/ # 서킷브레이커/리트라이 설정
│ ├── service/ # 동기 생성 + 비동기 처리 + Saga 보상
│ ├── domain/ # ShippingRequest 엔티티, ShippingStatus 상태머신
│ └── repository/ # JPA Repository
├── queue/ # 이벤트 대기열 (보강)
│ ├── service/ # Redis Sorted Set 대기열 + 토큰 발급
│ └── controller/ # 대기열 진입/순번/입장 API
└── common/
└── exception/ # ErrorCode, 예외 처리
READY → APPROVED → CAPTURED
↓ ↓ ↓
FAILED CANCELED CANCELED / PARTIAL_CANCELED
approve 시 가맹점이 요청한 금액과 PG가 실제 승인한 금액을 비교한다. 불일치 시 자동으로 PG 취소 후 FAILED 처리하여 잘못된 금액의 결제가 확정되는 것을 방지한다.
가맹점 → approve(10000원) → PG 승인(9000원) → 금액 불일치 → PG 자동 취소 → FAILED
Spring의 @Transactional은 프록시 기반이라, 같은 클래스 내부에서 메서드를 호출하면 트랜잭션이 적용되지 않는다 (self-invocation 문제). 이를 해결하기 위해 분산 락 담당(PaymentService)과 트랜잭션 처리 담당(PaymentTransactionService)을 분리했다.
PaymentService (분산 락) → PaymentTransactionService (@Transactional)
같은 orderId로 동시에 요청이 들어올 때 중복 결제를 방지하기 위해 Redis 분산 락을 사용한다.
서버A → Redis: "lock:payment:order-1" 획득 → 결제 처리
서버B → Redis: "lock:payment:order-1" 획득 시도 → 이미 있음 → 거부
- 왜 Redis인가?: 애플리케이션 레벨 락(synchronized, ReentrantLock)은 한 서버 안에서만 동작한다. 서버가 여러 대면 각 서버의 락이 독립적이라 동시 요청을 막을 수 없다. 외부 저장소(Redis)에 락을 두면 모든 서버가 같은 곳을 바라보기 때문에 서버 수와 관계없이 동시성 제어가 가능하다.
- SETNX: Redis의 원자적 연산. 키가 없을 때만 값을 설정하므로 동시 요청 중 하나만 성공한다.
- TTL: 락에 만료 시간을 설정하여 서버 장애 시 락이 영원히 안 풀리는 것을 방지한다.
가맹점이 네트워크 타임아웃 등으로 같은 결제를 재시도할 때 중복 결제를 방지한다.
1. 최초 요청: orderId="order-1", idempotencyKey="key-1" → 결제 처리
2. 재시도: orderId="order-1", idempotencyKey="key-1" → 기존 결과 리턴 (중복 결제 X)
3. 새 시도: orderId="order-1", idempotencyKey="key-2" → FAILED 상태면 재시도 허용
idempotencyKey는 가맹점이 생성하여 요청에 포함. DB에 UNIQUE 제약으로 유일성 보장.- 같은 키로 재요청 시 기존 결제를 그대로 리턴하고, 다른 키로 같은 주문 요청 시 DUPLICATE_ORDER 에러.
PG사 장애 시 타임아웃까지 대기하며 연쇄 장애가 발생하는 것을 방지한다. Resilience4j Circuit Breaker를 데코레이터 패턴으로 적용하여 기존 서비스 코드 변경 없이 PG 호출을 보호한다.
정상: PaymentTransactionService → CircuitBreakerPgConnector → MockPgConnector → 응답
장애: PaymentTransactionService → CircuitBreakerPgConnector → 서킷 OPEN → 즉시 503 응답
- 최근 10건 중 실패율 50% 초과 시 서킷 OPEN
- 30초 후 HALF_OPEN으로 전환, 3건 테스트 호출로 복구 판단
- PG 비즈니스 실패(잔액 부족 등)는 서킷에 영향 없음
PG 호출 시 일시적 네트워크 오류에 대해 Resilience4j Retry로 자동 재시도한다. 서킷 브레이커 안쪽에서 동작하여, 서킷 OPEN이면 재시도 없이 즉시 차단된다.
서킷 CLOSED: CircuitBreaker → Retry(최대 3회, 500ms→1s→2s) → PG 호출
서킷 OPEN: CircuitBreaker → 즉시 차단 (재시도 안 함)
- Exponential backoff: 500ms → 1s → 2s로 간격을 늘려 PG 서버 부하 방지
- 재시도 3회 모두 실패해도 서킷에는 실패 1건만 기록 (서킷이 바깥이므로)
- PaymentException(비즈니스 실패)은 재시도하지 않음
approve 시 PG A가 서킷 OPEN이면 자동으로 PG B로 fallback한다. capture/cancel은 approve에서 결정된 PG로만 요청한다.
approve: PG A (서킷 OPEN) → PG B (fallback) → 승인 성공 → Payment.pgProvider = "PG B"
capture: Payment.pgProvider 조회 → PG B로 매입
cancel: Payment.pgProvider 조회 → PG B로 취소
- approve: 라우터가 순서대로 PG를 시도, 서킷 OPEN인 PG는 건너뛰고 다음 PG로 fallback
- capture/cancel: DB에 저장된 pgProvider로 특정 PG를 지정하여 호출 (fallback 없음)
- 모든 PG가 서킷 OPEN이면
ALL_PG_UNAVAILABLE에러 반환
PG 커넥터를 인터페이스로 추상화하고, 서킷 브레이커를 데코레이터 패턴으로 적용했다. 서킷 브레이커 데코레이터는 어떤 PG 구현체든 감쌀 수 있도록 인터페이스에 의존하게 했고, 빈 조립은 Configuration에서 담당한다.
새 PG를 추가할 때 커넥터 구현체만 만들고 Config에 빈 등록만 추가하면 되고, 기존 코드는 변경할 필요가 없다.
PgConfig (빈 조립)
→ CircuitBreakerPgConnector (데코레이터: 서킷 브레이커 적용)
→ PgConnector (인터페이스: MockPgConnectorA, MockPgConnectorB, ...)
- 데코레이터 패턴: 기존 로직 변경 없이 서킷 브레이커 기능을 덧씌움
- 인터페이스 추상화: 구현체 교체/추가가 자유로움
- OCP(개방-폐쇄 원칙): 확장에는 열려있고, 기존 코드 수정에는 닫혀있음
POST /api/v1/payments/approve
{
"orderId": "order-001",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"amount": 10000
}POST /api/v1/payments/capture
{
"orderId": "order-001"
}POST /api/v1/payments/cancel
{
"orderId": "order-001"
}http://localhost:8080/swagger-ui/index.html
docker compose up -d./gradlew bootRun| 서비스 | 포트 |
|---|---|
| Spring Boot | 8080 |
| MySQL (Docker) | 3307 |
| Redis (Docker) | 6379 |
- 결제 승인/취소 API
- 결제 상태 머신
- 멱등성 처리 (idempotencyKey)
- PG 커넥터 추상화 + Mock PG
- Flyway 마이그레이션
- 동시성 제어 (Redis 분산 락)
- 결제 조회 API
- 매입(CAPTURED) 처리
- 금액 위변조 검증 (PG 승인 금액 불일치 시 자동 취소)
- Circuit Breaker
- PG 라우팅 (장애 시 다른 PG로 전환)
- 재시도 전략
- 부분 취소
- 복식부기 원장
- 대사 (Reconciliation)
- Kafka 기반 정산 배치
- 가맹점 정산금 계산
- 모니터링 (메트릭 수집, 알림)
- 웹훅 (PG → 가맹점 비동기 알림)
- 인증/인가 (가맹점 API Key)
- 테스트는 Testcontainers로 변경
윈들리 코딩테스트 리뷰에서 드러난 약점을 보강하기 위한 실습 문제.
각 문제는 problems/ 마크다운 + 실제 코드 구현으로 구성.
| # | 주제 | 파일 | 약점 매핑 |
|---|---|---|---|
| 01 | 한정 수량 동시성 제어 | 01-limited-stock-concurrency.md | 비관적/낙관적 락 트레이드오프, 고객당 제한 동시성 보장 |
| 02 | 외부 배송 API + 장애 격리 | 02-shipping-api-resilience.md | 동기/비동기 분리, Saga 보상, CORS 오답 교정 |
| 03 | 이벤트 트래픽 대기열 | 03-event-traffic-queue.md | 시나리오 특화 확장성, Redis 앞단 트래픽 제어 |
| 약점 | 코드 위치 | 핵심 |
|---|---|---|
| 락 전략 깊이 부족 | stock/service/StockOrderTransactionService.kt |
비관적/낙관적/원자적 UPDATE 3가지 구현 비교 |
| 고객당 제한 동시성 보장 부재 | stock/repository/StockOrderRepository.kt |
SUM + FOR UPDATE 트랜잭션 내 검증 |
| CORS 오답 | problems/02-shipping-api-resilience.md |
서버-서버 통신에서 CORS 무관 명시 |
| 동기/비동기 분리 미언급 | shipping/service/ShippingService.kt |
createShippingRequest(동기) vs processShipping(비동기) |
| Saga 보상 트랜잭션 부재 | shipping/service/ShippingService.kt |
배송 실패 시 SHIPPING_FAILED 보상 |
| 배송사 fallback 불명확 | shipping/router/ShippingRouter.kt |
PgRouter와 동일 패턴 적용 |
| 시나리오 특화 확장성 부족 | queue/service/EventQueueService.kt |
Redis Sorted Set 대기열 + 토큰 발급 |
| 프로젝트 | 스택 | 참고 영역 |
|---|---|---|
| Hyperswitch | Rust | PG 커넥터 추상화, 상태머신, PG 라우팅, 대사 |
| Kill Bill | Java | 구독 빌링/결제 |
| Blnk | Go | 복식부기 원장, 자동 대사 |
| samchon/payments | TypeScript | PG 통합 연동, 웹훅, 빌링 |