ASHD Dev_Blog

개발 일기 3편 : 결제 데이터 재조정!

개발 일기 3편

이재룡
이재룡 Mar 5, 2026

이전 내용 이어서 : 개발 일기 2편

 

[ 잘못된 데이터를 어떻게 수정할까? ]

<Reconciliation in “결제!”>

네트워크, 서버 장애 등 다양한 원인으로 DB에 정상 데이터가 입력되지 않을 수 있다!

하지만 이전에 언급한 것처럼 부분적 트랜잭션을 보장하는 구조에서는

 

고려해야 할 결과는 한정적이다

오로지 paymentEventPG사의 데이터 기록만이 서로 다른 status를 가질 때

Reconciliation을 하면 된다!

 

예를 들어

우리 데이터는 PENDING인데 PG사 데이터 기록은 PAID인 경우

PG사 데이터 기반으로 PENDING → SUCCESS인 데이터를 append해보자

 
 

[ 크레딧까지 결과 전파? ]

< 1. 크레딧은? >

이전에 논의했듯이, credit은 단순히 카운트에 불과하지만

우리는 credit의 공급 및 사용에 대한 원장 데이터를 기록하기 때문에

 

Event를 새로 append 하게 되면

  • paymentEvent status가 바뀐 새로운 ROW append
  • paymentOrder의 status 변경
  • creditAccount의 발급 또는 발급 취소로 인한 개수 변경
  • creditLedger에 새로운 ROW append

가 순차적으로 진행된다.

 

기본적인 전제조건은

  • (paymentEvent와 paymentOrder) / (creditAccount와 creditLedger) 각각은 같은 트랜잭션!
  • payment가 credit에 영향을 주는 거는 success 와 refund 뿐!
 
 

< 2. 어떻게? >

제외했던 방법
  • Transaction Outbox Pattern : 인프라적 비용 문제로 이후에 채팅 서비스에서 많관부 ㅎㅎ
  • 비동기 이벤트 통신 : MQ 등을 활용하기 때문에 비용적 문제 + 크레딧이 비동기 적으로 들어오는건 좀 아니지 않나?
 

일단 크게 두 가지 선택 사항이 있다.

  • Transaction을 묶는 방법
  • 각각을 transcation으로 놓고 크레딧 실패시 Retry 로직 또는 스케줄러
 

무조건적으로 트랜잭션을 거는게 좋은 방법인거 같지만, 중요한 문제가 있다

결제 성공 상황을 조작하는 HandleSuccess에서 한 트랜잭션으로 크레딧을 발급을 묶으면,

크레딧 발급이 실패하는 상황이 발생 했을 때,

 

트랜잭션 rollback 과정에서 결제 성공 → 대기 상태로 변경 되는 문제가 있다.

서버 자체적으로는 부분적 원자성을 보장한다!

⇒ 단, 결과적으로 DB의 상태와 Portone의 상태가 CreditService의 성공 여부에 따라 달라진다 ㅠㅠ

 

두번째 각각을 트랜잭션으로 놓게 되면,

크레딧 발급 성공/실패와는 상관 없이 결제 부분과 portone 데이터가 서버 장애 등 문제 제외, 정상 상태가 보장된다

 

하지만, 크레딧 발급 과정의 실패 + 결제는 성공이라는 부분적 원자성이 깨진다.

⇒ 결제와 portone의 데이터의 정합성을 보장한다!

⇒ 단, 서버 자체적으로는 부분적 원자성은 깨진다 ㅠㅠ

 
 

< 3. 각각에 대한 보정 >

먼저 두번째 케이스에 대해서 생각을 해보면,

부분적 원자성을 되찾기 위해서

결제 성공 데이터를 기반으로 무조건 적으로 크레딧 발급을 시도해야한다

결국 성공했다면 크레딧을 무조건 넣어야한다 creditAccount + creditLedger 둘다!

 

아무리 retry로직을 넣는다고 해도, 서비스가 성공한다는 보장이 없고,

  • 이미 부분적 원자성이 깨진 상태가 발생한다는게 큰 리스크라고 생각한다!

즉 아무리 retry를 하더라도 결과적으로 db상태가 payment와 credit에서 이상 현상이 발생할 수 있다

 
 

그에 반해 첫번째 케이스의 경우는

중요! : Reconciliation에 동작에 대한 정의 (이후 내용 참고!)

Reconciliation을 실행해서, N회차 재시도를 하는 과정에서

  • 결과적으로 실패를 하더라도 부분적 원자성이 보장된다.
  • 더 나아가, 주기적으로 배치를 이용해서 Reconciliation하기도 하고,

기회가 더 많은 느낌? + 기존에 있는 로직을 적용하는 방식이기 때문에 transactional outbox의 CDC/Relay와 같은 추가 인프라 요소가 없다!

 

 

[ Reconciliation는 어떤 동작을? ]

가장 큰 의문점

 

1안 : DB에 직접 Write

⇒ Batch를 통해 ItemReader/ItemWriter를 활용하여 DB단에서 PaymentEvent에 올바른 status 주입

이 동작은 <3.보정>페이지의 두번째 로직과 유사한 결과를 도출한다.

  • 무조건 참(PG)이 되도록 DB데이터 Append
  • PG사와 paymentEvent를 동기화 할 수는 있으나, 그럼 credit은?
  • 그럼 또 retry 로직을?

 

2안 :

⇒ Batch를 통해서 ItemReader 기반 paymentHandler 동작

이 동작은 <3.보정>페이지의 첫번째 로직과 유사한 결과를 도출한다.

  • PG데이터를 기반으로 DB데이터를 동기화하는 과정인데 이 과정이 실패가 있어도 되나?
  • Reconciliation은 마지막 최후의 보루인데 이것조차 실패할 수 있다는 게 맞나?
 
 
 

[ Event 기반 이상적인 로직? ]

일단 내가 생각하는 가장 이상적인 로직은

Reconciliation 2안 즉 Batch의 ItemWriter 동작 전에 handler를 실행하고 이를 기반으로 SpringEvent로 동기 처리하는 로직이다!

 

예를 들어

  • paymentEvent : PENDING
  • PG : PAID 일 때
 

SuccessHandler가 동작하면

  • paymentEvent (Pending → Success) [append]
  • paymentOrder (status : 결제 성공)

까지 1 Transaction

 

SpringEvent 동기 처리 = 인메모리 이벤트 처리

@EventListener

  • creditAccount
  • creditLedger

까지 2 Transcation이되,

 

EventListener를 통해 1 Tx 2 Tx 가 사실상 하나의 트랜잭션으로 묶인다.

 

단점은 강한 결합

즉, 리스너에서 에러가 발생하면, 메인 로직의 작업도 함께 롤백(Rollback) 된다는 것인데,

  • 이는 Reconciliation을 재시도 하는 등으로 보정할 수 있는 추가 기회를 가질 수 있고 (DB의 부분적 원자성이 보장되기 때문!)
  • 인 메모리에 저장된 이벤트가 삭제되더라도 결국 Reconciliation의 로직 자체가 HandleSuccess 부터 재시도기 때문에 인메모리 이벤트를 재발행!
 

결국

“Reconciliation은 마지막 최후의 보루인데 이것조차 실패할 수 있다는 게 맞나? “

라는 문제점은 남음 ㅠㅠ

 
 

[ Reconciliation의 케이스를 분석해보자! ]

portone data status

  1. ready (미결제)
      • 결제 대기 상태
  1. paid (결제 완료)
      • 일반 결제가 성공적으로 승인
  1. cancelled (결제 취소)
      • 관리자 콘솔이나 API를 통해 결제가 취소(환불)된 상태
  1. failed (결제 실패)
      • 결제 승인에 실패했거나
      • 고객이 결제창을 닫아 이탈
       

paymentEventStatus

READY 미결제(결제창 진입) e.g. 브라우저 창 이탈, 가상계좌 발급 완료 등

SUCCESS 결제 승인 성공 (포트원 paid) e.g. 가상계좌 결제 입금, 예약 결제 시도 등

FAILED 결제 승인 실패 (포트원 failed) e.g. 신용카드 한도 초과, 체크카드 잔액 부족, 브라우저 창 종료 또는 취소 버튼 클릭

FORGERY 결제 위변조 감지

REFUND_REQUESTED 환불 시도 (Pending)

REFUND_FAILED 환불 거절, e.g. 한도 초과/기간 만료, 중복 환불, PG사/은행 점검

REFUND_SUCCESS 환불 완료 = 결제 취소(포트원 cancelled) e.g. 관리자 콘솔 결제 취소

ALL

케이스 고민에 대한 설명

  • [”?”]의 경우 PG사의 상태
  • PaymentEventStatus : status1 → status2의 경우
    • status2와 PG사의 [?]가 일치하는게 맞는데
    • status1의 상태일 경우 reconciliation 실행을 통해 status2로 변경 (Handler 동작)
 

케이스 고민

  • READY → SUCCESS [PAID] : 성공 무시
  • SUCCESS → READY [READY] : 이 케이스가 가능한가?
  • READY → FAILED : [FAILED] : 실패 무시
  • REFUND_REQUESTED → REFUND_SUCCESS [CANCELLED?] : 환불 성공 무시?
  • REFUND_REQUESTED → REFUND_FAILED [PAID] : 환불 실패 무시?
 
 

추천 글

BlogPro logo