ASHD Dev_Blog

개발 일기 1편 : Credit 시스템 개편!

개발 일기 1편

이재룡
이재룡 Feb 21, 2026

[ 메인 과제 ]

우리 팀에서 주기적으로 단기 프로젝트 성으로 진행하던 유저 매칭 서비스를 상시 운영 서비스로 바꿔보려고 한다.

 

그럼 바뀌는게 뭘까?

  1. 데이터를 기반으로 최적해를 매칭하고, 결과를 배포한 이전 방식과 달리, 매 순간마다 기준에 따라 매칭 데이터를 순위 별로 만들어야하는 방식
  1. 참여를 위해 단순 결제만 하던 방식이 아니라, 사용자가 credit을 활용해 원하는 동작을 하게하는 시스템
  1. 결제 데이터 정합성 → 이거는 이전 시즌을 운영할 때 문제가 많았다 ㅠㅠ
  1. 채팅 서비스 도입 → 이것도 지금 참 난관이다
 

[ 이전에는 왜 결제에서 문제가 발생했나? ]

1. 순차적으로 처리 검증이 안됐다

일단 결제 시스템을 대충만 봐도 백엔드에서 여러 도메인의 복합 동작이 필요하다.

  • 매칭 신청 → 결제 창 → 백엔드에서 결제 + 매칭 상태 변경 + 포트원 api → 결제 완료/실패

 

그러다보니,

  • 백엔드에서는 결제 완료라고 했는데,
  • 포트원에서는 아직 처리중이고,
  • 매칭 상태는 먼저 변경되고,
  • 다시 Verify 과정으로 확인해보니 이미 매칭 상태는 이미 결제가 되었는데?

이런 상황이 단순 개발 환경에서 테스트 할때는 발생하지 않았지만, 실제 운영환경에서는 멀티스레딩 상황에서?는 문제가 발생했다 ㅠㅠ

절대적인 개수 자체가 많지는 않아서 수동으로 맞췄는데 저거 백업하고 맞추는 게 너무나 힘들었던 기억이 있다.

 

반성 : 이전 시즌은 내가 결제 담당이 아니었기 때문에 이해가 부족하기도 했고, 매칭 쪽을 개발한 나의 입장에서는 메소드를 넘겨주고 테스트 환경에서 “동작이 잘 되네” 에서 깊게 생각하지 않았던 것 같다.

 

2. Verify를 포트원에 의존한다 + 재시도가 해결방법?

기존 로직은 포트원에서 웹훅 받고 db 한번 보고 결제가 완벽하다 판단했다

이게 완벽히 정합성을 챙길 수 있는가? 라는 고민을 해보니 답은 “아니다”였다!

그럼 앞에서 언급한 에러처럼 이미 결제가 시작했는데 후에 verify에서 어긋나면 그냥 캔슬하면 그만인가?

 
 
 

[ 그럼 어떻게 바꿀까? - 사전 단계 : Event와 Order의 분리 ]

기존 payment 하나를 통합으로 사용하고 있던 구조를 분리!
  • Payment-Order (결제 주문 - 핵심 도메인)
    • 역할: 우리 서비스에서 결제되고, 상품의 소유가 바뀌고를 DB에 기록
  • Payment-Event (결제 이벤트 - PG 연동 인프라)
    • 역할: 외부 PG사(PortOne)와의 통신 이력(결제 성공/실패/환불/검증 webhook 등의 결과)을 기록
    •  

그래서 나뉜게 무슨 장점을 가지느냐

  • "결제 실패 후 재시도" 처리가 매우 깔끔해진다

분리했을 때는 Payment-Order는 하나만 존재하고, 그 아래에 Payment-Event가 2줄(1. FAILED 기록, 2. SUCCESS 기록) 쌓이게 된다

즉 결제 기록을 계속해서 재발급 하는 것이 아닌 event 즉 PG사 요청만 번복하면 만사 해결!

 
  • Verify 처리 로직이 단순해진다.

기존에는 물건정보와 결제정보가 한번에 관리되다 보니까 중복되었다고 결제를 취소하는 과정이

물건 발급 → 사용자 소유로 변경 등의 모든 로직이 진행된 상황에서

역방향으로 돌려야했다면, 포트원 웹훅을 imp_uid 가 unique되게 함으로써, 무조건 1개를 유지하고 중복이나, 문제가 발생했을 때는 순서상 로직 자체가 진행이 안된다!

→ event 처리 즉 A이후에 B가 되게 하는게 코드 단에서 설정해야했다면, 애초에 Payment-Event DB단에서 차단!

 

즉 결제 과정은

  1. 우리 서버에서 merchant_uid 발급 = payment-order is PENDING
  1. 프론트에서 결제
  1. 포트원에서 return imp_uid to Front
  1. 우리 서버에서 payment-event를 imp_uid를 사용해서 저장
  1. payment-order is SUCCESS

→ 후에 event verify를 할때는 imp_uid 로 Portone API 사용

순서다

 

여기에 서비스의 정합성을 위해 몇 개의 로직을 이번에 추가 해보았다

물론 정합성이 맞게 애초에 PAYMENT에서 문제가 없다면 필요가 없겠지만

여러 호출을 통해 동작하므로, 꼬일 경우를 대비한 보호 및 검증 설계를 추가하자

 
 

[ 그럼 어떻게 바꿀까? - 1단계 : Ledger의 도입 ]

절대 장부(원장)를 만들자!

Ledger는 Sorce Of Truth로 무조건적인 신뢰!

  • 크레딧 구매 요청부터
  • 결제를 요청하고, 검증하면서
  • 실제 크레딧 사용까지를 모든 유저를 대상으로 기록한 장부를 도입하자
 

그리고 스케줄러를 사용해서 주기적으로 확인

문제가 발생되면, 무조건 Ledger 기준으로 DB를 수정하면

스케줄러 이후에는 절대적으로 무결한 데이터가 db에 형성될 수 있다!

→ Ledger기반으로 order, event 등의 payment관련 기록을 재조정하는 로직이 구현이 제일 어려울 거 같긴한데 다른 팀원이 맡아서 열심히 하는 중이다 ㅎㅈㅈ ㅎㅇㅌ!

 

[ 그럼 어떻게 바꿀까? - 2단계 : Credit의 도입 ]

결제 과정에서 가장 큰 문제가 뭘까 고민해봤더니 역시 유저의 돈 문제이다

  • 유저의 돈만 빠져나가고 크레딧이 안 들어오거나,
  • 한 번 결제했는데 크레딧이 두 번 들어오는 일은 절대 일어나서는 안된다

이거는 원장으로 다 돌리면 해결되나?

 

→ 이것은 맞는 말이다

  • 스케줄러로 (원장 데이터 = DB데이터)가 되는 순간 데이터는 완벽하게 정합성을 보장한다
 

근데 매 결제 타이밍 / 크레딧 사용 타이밍 원장 수준에서의 검사가 계속 되는게 맞는 구조인가?

에 대한 답은 “아니다” 라고 생각한다.

 

“원장”이라함은 절대적 Truth를 가지는 대신에, 모든 기록이 다 적혀있는 매우 큰 장부다 이걸 매 요청마다 n번씩 확인하면서 진행하는 것은 너무 비효율적이다.

 

그럼 매 결제마다 원장 단계 이전에 검증을 한차례 진행하려면?

비트 코인 방식에서 착안해서, 크레딧 자체에 상태 정보를 기록하자!

  • 크레딧에 순차적 상태값을 부여하고, 이를 이전 단계까지의 검증 완료 기준으로 삼아보자
 

Credit 설계

  • 크레딧을 구매하는 수만큼 크레딧 생성!
  • 각 크레딧은 bulk 단위로 동작하면서
  • 상태 및 소유자, 구매 물품, 개당 가격 등의 기본 정보를 소유
  • 기본 정보를 바탕으로 paymentService에서 엔티티 변경 내역 기반 정보를 받아서
  • 크레딧의 정보와 비교 및 검증
 
추가 설명

과정에 대해 예시를 들어서 조금 더 설명해보면

  • 먼저 유저 A가 크레딧 5개 구매요청 → 5개의 빈 크레딧을 생성!
    • 크레딧에 [유저 A가 발급]이라는 정보를 넣고 X 5
  • 사용자가 클라이언트를 이용해서 결제를 시도할 때, 실제 유저 A가 merchant_id를 통해 실제 payment-order 정보 기반 검증
    • 실제로 5개인가 검증
  • 사용자가 구매를 시도할 때, 실제 PG와 통신 기록인 payment-event 확인 및 검증
  • 만약 포인트 같은 게 있다면 포인트가 해당 유저가 소유값을 db에서 확인 후에 1000이 차감되었는지도 확인 필요! → 일단 지금은 고려하지말기!
    • 검증
      • 소유자 확인 = 결제자
      • 실제로 pending인게 5개인가?
      • 개별가격 X 개수 = 실제 결제 금액
    • 크레딧에 [유저 A 결제 완료] 기록
    •  
  • 사용자가 크레딧을 상품 구매에 사용할 때,
    • 검증
      • 소유자 = 구매요청자
      • A가 활성 크레딧(결제 검증까지 된)을 상품 1의 가격 보다 많이 소유하고 있는지
      • 실제로 그 가격 만큼 차감되었는지 등을 확인 후에
    • 크레딧에 [유저 A 상품1 구매] 기록
    •  
  • 최종 기록
    • 검증
      • 최종 기록 구매 기록 및 소유자
      • 상품 1번이 유저 A 소유로 잘 이전되었는지
      • 상품 DB에서 1번의 갯수가 1개 차감 되었는지 등을 확인
    • 크레딧에 [사용 완료]
 

이전 과정이 실패 했을 때는 어떻게 해야 하나? ⇒ 원장을 기반으로 DB table의 정합성을 맞추는 과정을 거치면 될 것 같다.

 

추천 글

BlogPro logo