Skip to content

young0264/payments

Repository files navigation

Payments

여러 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

서비스 클래스 분리 (PaymentService / PaymentTransactionService)

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 에러.

Circuit Breaker (PG 장애 격리)

PG사 장애 시 타임아웃까지 대기하며 연쇄 장애가 발생하는 것을 방지한다. Resilience4j Circuit Breaker를 데코레이터 패턴으로 적용하여 기존 서비스 코드 변경 없이 PG 호출을 보호한다.

정상: PaymentTransactionService → CircuitBreakerPgConnector → MockPgConnector → 응답
장애: PaymentTransactionService → CircuitBreakerPgConnector → 서킷 OPEN → 즉시 503 응답
  • 최근 10건 중 실패율 50% 초과 시 서킷 OPEN
  • 30초 후 HALF_OPEN으로 전환, 3건 테스트 호출로 복구 판단
  • PG 비즈니스 실패(잔액 부족 등)는 서킷에 영향 없음

재시도 전략 (Retry)

PG 호출 시 일시적 네트워크 오류에 대해 Resilience4j Retry로 자동 재시도한다. 서킷 브레이커 안쪽에서 동작하여, 서킷 OPEN이면 재시도 없이 즉시 차단된다.

서킷 CLOSED: CircuitBreaker → Retry(최대 3회, 500ms→1s→2s) → PG 호출
서킷 OPEN:   CircuitBreaker → 즉시 차단 (재시도 안 함)
  • Exponential backoff: 500ms → 1s → 2s로 간격을 늘려 PG 서버 부하 방지
  • 재시도 3회 모두 실패해도 서킷에는 실패 1건만 기록 (서킷이 바깥이므로)
  • PaymentException(비즈니스 실패)은 재시도하지 않음

PG 라우팅 (장애 시 자동 전환)

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 커넥터를 인터페이스로 추상화하고, 서킷 브레이커를 데코레이터 패턴으로 적용했다. 서킷 브레이커 데코레이터는 어떤 PG 구현체든 감쌀 수 있도록 인터페이스에 의존하게 했고, 빈 조립은 Configuration에서 담당한다.

새 PG를 추가할 때 커넥터 구현체만 만들고 Config에 빈 등록만 추가하면 되고, 기존 코드는 변경할 필요가 없다.

PgConfig (빈 조립)
  → CircuitBreakerPgConnector (데코레이터: 서킷 브레이커 적용)
    → PgConnector (인터페이스: MockPgConnectorA, MockPgConnectorB, ...)
  • 데코레이터 패턴: 기존 로직 변경 없이 서킷 브레이커 기능을 덧씌움
  • 인터페이스 추상화: 구현체 교체/추가가 자유로움
  • OCP(개방-폐쇄 원칙): 확장에는 열려있고, 기존 코드 수정에는 닫혀있음

API 스펙

결제 승인

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"
}

Swagger UI

http://localhost:8080/swagger-ui/index.html

실행 방법

1. Docker로 MySQL/Redis 실행

docker compose up -d

2. 앱 실행

./gradlew bootRun

환경 정보

서비스 포트
Spring Boot 8080
MySQL (Docker) 3307
Redis (Docker) 6379

로드맵

Phase 1 — 결제 핵심 (현재)

  • 결제 승인/취소 API
  • 결제 상태 머신
  • 멱등성 처리 (idempotencyKey)
  • PG 커넥터 추상화 + Mock PG
  • Flyway 마이그레이션
  • 동시성 제어 (Redis 분산 락)
  • 결제 조회 API
  • 매입(CAPTURED) 처리
  • 금액 위변조 검증 (PG 승인 금액 불일치 시 자동 취소)

Phase 2 — 내결함성

  • Circuit Breaker
  • PG 라우팅 (장애 시 다른 PG로 전환)
  • 재시도 전략
  • 부분 취소

Phase 3 — 원장/대사

  • 복식부기 원장
  • 대사 (Reconciliation)

Phase 4 — 정산/빌링

  • Kafka 기반 정산 배치
  • 가맹점 정산금 계산

Phase 5 — 운영

  • 모니터링 (메트릭 수집, 알림)
  • 웹훅 (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 통합 연동, 웹훅, 빌링

문서

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages