결제 시스템에서 가장 어려운 부분은 결제 확정(Confirm Payment) 단계
Pay 잔액 차감 / 포인트 사용 or 적립 / 트랜잭션 상태 확정 / 정산 데이터 기록 등 여러 상태가 동시에 변경됨
IF. MSA 환경에서 네트워크 장애, 중복 요청, 다중 인스턴스 환경, 서비스 간 호출 실패
-> 상태만 성공이고 금액은 차감되지 않은 불일치 문제가 발생할 수 있음
=> 원장(Ledger) 정합성 문제
-> 결제 확정 = Ledger 변경이 성공했을 때만 상태를 성공으로 전이
*결제 시스템 개요 확인
[PlantiFy] 결제 서비스(Pay & Transaction & Payment Service) - MSA 환경에서 결제 시스템 구축하기 1 / Redis 분
결제 서비스 개요자체 결제 수단을 운영하고, 이를 기반으로 결제, 환불, 정산까지 처리하는 전용 페이먼트 플랫폼외부 PG나 카드망을 거치지 않고, 내부 원장과 트랜잭션 상태를 직접 관리 단일
debug.tistory.com
Redis 싱글 락의 한계
- Redis `SETNX + TTL`
- 락 key: `user:{userId}`
- 락 value(UUID) 저장하지만 검증 X
- unlock 시 `DEL key`
@Component
@RequiredArgsConstructor
public class RedisLock {
private final StringRedisTemplate stringRedisTemplate;
public boolean tryLock(String key, long timeoutMillis) {
String value = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofMillis(timeoutMillis));
return Boolean.TRUE.equals(success);
}
public void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
문제점
1) 락 소유자 미검증 문제
락 value로 UUID를 저장하지만, unlock 시 해당 value를 검증하지 않고 단순히 `DEL key` 수행
다음 같은 시나리오 발생할 수 있음
1. A 서버가 락 획득 후 처리 중 GC 또는 네트워크 지연
2. TTL 만료
3. B 서버가 동일 key로 락 획득
4. A 서버가 finally 블록에서 DEL key 실행
→ B 서버의 락 삭제
=> 결과적으로 동시에 2개의 결제 처리 가능해짐
2) TTL 기반 락의 불안정성
TTL은 예상 실행 시간에 의존
- 처리 시간 > TTL -> 락 풀림
- TTL을 길게 잡으면 장애 시 락이 오래 유지
=> 락 안정성과 장애 복구 중 하나를 포기해야 함
3) 단일 Redis 의존성
Redis 장애 or Failover 상황에서
- 락 정보 유실
- 중복 결제 처리 가능
- 정합성 보장 불가
=> 로컬 뮤텍스에 가까운 구현
해결: Redisson 기반 분산 락 + Redlock
Redisson의 RLock과 RedLock 알고리즘 도입해 금전 원장(Ledger)의 정합성 보장
특징
Redisson RLock
- 락 소유자 검증
- threadId / UUID 기반으로 락 소유자 식별
- 다른 스레드/인스턴스는 unlock 불가
- Lua Script 기반 안전한 unlock
- `DEL`이 아닌 원자적 비교 후 삭제
- 락 탈취 문제 방지
- Watchdog으로 TTL 자동 연장
- 락을 잡고 있는 동안 TTL 자동 갱신
- GC / 네트워크 지연에도 안전
RedLock
다중 Redis 인스턴스 기반 분산 락 알고리즘
- N개의 Redis 중 과반수 락 획득 시 성공
- 일부 Redis 장애 허용
- 락 신뢰성 강화
코드 확인
implementation 'org.redisson:redisson-spring-boot-starter:3.40.0'
Redisson 기반 `LockProvider`
@Component
@RequiredArgsConstructor
public class LockProvider {
private final RedissonClient redissonClient;
public RLock getLedgerLock(Long userId) {
return redissonClient.getLock("ledger:" + userId);
}
}
RLock lock = lockProvider.getLedgerLock(userId);
lock.tryLock(3, 10, TimeUnit.SECONDS);
도메인 경계에 따른 분산 락 설계
락의 기준은 요청이 아니라 도메인 책임
Pay Service — 금전 원장 보호
- 락 키: `ledger:{userId}`
- Pay + Point를 하나의 논리적 원장으로 취급
- 결제 / 환불 / 충전 모두 동일 락 사용
=> 금전 상태는 동시에 변경될 수 없음
Transaction Service — 상태 전이 보호
- 락 키: `transaction:{userId}`
- `PENDING → PAYMENT / FAILED` 중복 방지
- Scheduler / Kafka Consumer에서도 동일 락 사용
=> 상태 전이는 한 번만 일어나야 함
Payment Service — 실행 로그 보호
- 락 키: `payment:{userId}`
- 실제 돈은 움직이지 않지만
- 결제/환불/취소 중복 실행되면 안 됨
Pay Service
책임 분리
Controller
↓
PayFacadeService
↓
┌───────────────────────────────┐
│ PaymentOrchestrator │
│ RefundOrchestrator │
│ CancellationOrchestrator │
└─────────────┬─────────────────┘
↓
LedgerService (단일 원장)
↓
Pay / Point (Atomic)
=> 결제 흐름은 Orchestrator가 조율하고, 금전 변경은 LedgerService가 단일 책임으로 보장
/ 각 서비스는 자신의 로컬 트랜잭션만 관리
`PayFacadeService`: Pay 서비스의 진입점
외부 요청을 내부 유스케이스로 위임하는 계층 / API 계층과 도메인 흐름 분리
- Controller -> 내부 로직 연결
- 결제 / 환불 / 취소 유스케이스 선택
- Orchestrator / Query 계층 분기
`PaymentOrchestrator`: 결제 사가의 중앙 조정자(Coordinator)
여러 로컬 트랜잭션 순서대로 실행 및 확정
ledgerService.debit(userId, finalAmount, pointToUse);
transactionClient.updateTransactionToSuccess(...);
paySettlementService.savePaySettlement(...);
- 결제 실행
- Transaction 서비스 호출
- Ledger 차감
- Settlement 기록
`LedgerService`: 금전 상태 변경을 담당하는 원장 서비스
Pay와 Point를 하나의 논리적 트랜잭션으로 처리
- Pay 잔액 차감 / 복구
- Point 차감 / 적립
- 분산 락(Redisson) 기반 동시성 제어
- `userId` 기준 단일 락
`PayQueryService`: Pay 도메인의 단순 조회책임을 가진 경량 Query 서비스
- Pay 잔액 조회
- 잔액 유효성 검증
Phase 정리
Phase 1 - 결제 시작 (Pending 생성 + Redirect 준비)
POST /v1/payment
- 트랜잭션 상태 `PENDING`
- 결제 클라이언트로 이동하기 위한 redirect + 검증용 token 생성

Phase 2 - 결제 확정 (Ledger 차감 + 상태 확정)
GET /v1/payment
- 실제 결제 확정
- Ledger 락 획득 (`ledger:{userId}`)
- 원장 차감
- 트랜잭션 상태 `PAYMENT`
- 정산 데이터 기록

참고
CQRS(Command Query Responsibility Segregation)
- 데이터 저장소로부터 읽기와 업데이트 작업 분리하는 패턴
- 명령(command) ~> 데이터 쓰기 / 쿼리(Query) ~> 데이터 읽기
- (+) 애플리케이션 퍼포먼스, 확장성, 보안성 ↑
기타
사가 패턴 - 코레오그래피 기반 사가 / 오케스트레이션 기반 사가 & 2PC
사가패턴(Saga Pattern) 복잡한 트랜잭션 → 서비스 단위로 분산시키기 위해 적합한 패턴트랜잭션 중 오류 발생 → 보상 트랜잭션 ~> 이전 단계 취소 ⇒ 데이터 일관성 보장 1) 코레오그래피 기반 사
debug.tistory.com
깃허브
플랜티파이
플랜티파이 has 21 repositories available. Follow their code on GitHub.
github.com
GitHub - hk-plantify/pay-service: [Java] Account, Pay, Point 등 페이 결제 관련 서비스(+ transaction, payment repo)
[Java] Account, Pay, Point 등 페이 결제 관련 서비스(+ transaction, payment repo) - hk-plantify/pay-service
github.com
GitHub - hk-plantify/transaction-service
Contribute to hk-plantify/transaction-service development by creating an account on GitHub.
github.com
GitHub - hk-plantify/payment-service
Contribute to hk-plantify/payment-service development by creating an account on GitHub.
github.com