Spring 개발일지

[팀프로젝트] MSA 기반 티켓팅 프로그램 - 결제 도메인 설계 및 API 구조 정리

김둘리 2026. 4. 20. 10:40

1. 결제 도메인 설계 방향

결제 도메인은 단순히 “결제 성공 여부”만 관리하는 것이 아니라, 상태 변화와 외부 PG 연동까지 포함하는 핵심 도메인이다.

이번 프로젝트에서는 다음 기준으로 설계했다.

  • 결제 상태는 명확한 상태값으로 관리한다
  • 상태 변경 이력은 별도로 관리한다
  • 외부 PG(토스)와의 통신은 서버에서 검증한다
  • 서비스 간 통신은 이벤트 기반으로 분리한다

2. Payment / PaymentHistory 구조

결제 도메인은 하나의 애그리거트로 구성된다.

  • Payment: 현재 상태를 관리하는 Aggregate Root
  • PaymentHistory: 상태 변경 이력을 기록하는 엔티티

역할 구분

Payment는 “현재 상태”를 의미한다.

PENDING / SUCCESS / FAILED / REFUNDED
 

PaymentHistory는 상태가 변경될 때마다 기록이 쌓인다.

예를 들어 다음과 같은 흐름이 발생한다.

PENDING → SUCCESS
 

이 경우:

  • Payment.status = SUCCESS
  • PaymentHistory에는 PENDING, SUCCESS 두 개의 기록이 존재

이 구조를 통해 결제 흐름을 추적할 수 있고, 장애 발생 시 원인 분석이 가능하다.


3. 테이블 설계

Payment

P_PAYMENT
────────────────────────────
PK id UUID
FK booking_id UUID
FK user_id UUID

order_id VARCHAR(100) UNIQUE
payment_key VARCHAR(200)

original_amount BIGINT
discount_amount BIGINT
final_amount BIGINT

status VARCHAR(20)
// PENDING, SUCCESS, FAILED, REFUNDED

requested_at TIMESTAMP
approved_at TIMESTAMP

created_at TIMESTAMP
updated_at TIMESTAMP
 

PaymentHistory

P_PAYMENT_HISTORY
────────────────────────────
PK id BIGINT
FK payment_id UUID

status VARCHAR(20)
reason_code VARCHAR(50)
reason VARCHAR(255)

created_at TIMESTAMP
 

4. order_id 설계 이유

order_id는 내부에서 생성하는 결제 식별자다.

이 값은 다음 목적을 가진다.

  • 중복 결제 방지 (Idempotency)
  • 결제 요청과 승인 매칭
  • 외부 PG와의 데이터 검증 기준

사용자가 결제 버튼을 여러 번 누르거나 네트워크 재시도 상황에서도 동일한 order_id를 사용하면 중복 결제를 방지할 수 있다.


5. status와 reason 분리

결제 상태는 enum으로 관리하고, 실패 사유는 별도로 관리한다.

  • status: 시스템 로직에서 사용하는 상태값
  • reason: 사람이 확인하는 메시지
  • reason_code: PG에서 내려주는 코드

예시:

status = FAILED
reason_code = INSUFFICIENT_BALANCE
reason = 잔액 부족
 

토스 결제 API에서도 실패 코드와 메시지를 제공하기 때문에 이를 그대로 저장하도록 설계했다.


6. API 구조 설계

API는 외부 API와 내부 API로 분리했다.

외부 API

/api/v1/...
 
  • 클라이언트가 호출
  • 사용자 인증 필요
  • 사용자 기능 제공

내부 API

/internal/v1/...
 
  • 서비스 간 호출
  • 외부 접근 불가
  • 시스템 처리용

이 구조를 통해 보안과 책임을 명확하게 분리했다.


7. 결제 API 명세

결제 요청

POST /api/v1/payments
 
  • 결제 요청 생성
  • Payment 상태를 PENDING으로 저장

결제 승인 (토스 confirm)

POST /api/v1/payments/confirm
 
  • paymentKey, orderId, amount 전달
  • 서버에서 토스 API 호출 후 검증
  • 성공 시 SUCCESS 처리 및 이벤트 발행

결제 조회

단일 조회 (구매자)

GET /api/v1/payments/{paymentId}
 
  • 본인 결제만 조회 가능

내 결제 목록 조회

GET /api/v1/payments/me
 

관리자 전체 조회

GET /api/v1/admin/payments
 

호스트 프로그램별 조회

GET /api/v1/host/programs/{programId}/payments
 
  • 해당 프로그램의 결제 내역 조회
  • 프로그램 소유자 검증 필요

환불

POST /api/v1/payments/{paymentId}/refund
 
  • 결제 취소 및 REFUNDED 상태 변경

8. 내부 API

결제 생성

POST /internal/v1/payments
 
  • 예매 서비스에서 호출
  • Payment 생성 (PENDING)

결제 상태 조회

GET /internal/v1/payments/{paymentId}
 
  • 다른 서비스에서 결제 상태 확인

상태 변경 처리

PATCH /internal/v1/payments/{paymentId}/status
 
  • 이벤트 기반 상태 변경

9. 결제 흐름

성공 흐름

결제 요청 → PENDING
→ 토스 결제 진행
→ confirm 호출
→ SUCCESS
→ PaymentHistory 기록
→ Kafka 이벤트 발행
→ 예매 확정
 

실패 흐름

PENDING
→ FAILED
→ PaymentHistory 기록
→ 좌석 해제 이벤트
 

10. 정리

결제 도메인은 단순 CRUD가 아니라 상태 흐름과 외부 시스템 연동을 포함하는 중요한 영역이다.

이번 설계에서 중점적으로 고려한 부분은 다음과 같다.

  • Payment와 PaymentHistory를 분리하여 상태와 이력 관리
  • order_id를 통한 중복 결제 방지
  • status와 reason을 분리하여 시스템과 사용자 관점 구분
  • 외부 API와 내부 API 분리를 통한 책임 분리
  • Kafka 이벤트를 통한 서비스 간 결합도 감소

이 구조를 기반으로 구현하면 확장성과 안정성을 모두 확보할 수 있다.