11장 결제 시스템
1단계: 문제 이해 및 설계 범위 확정
- 기능 요구사항
- 대금 수신(pay-in) 흐름: 결제 시스템이 판매자를 대신하여 고객으로부터 대금을 수령
- 대금 정산(pay-out) 흐름: 결제 시스템이 전 세계의 판매자에게 제품 판매 대금을 송금
- 비기능 요구사항
- 신뢰성 및 내결함성: 결제 실패는 신중히 처리해야 한다.
- 내부 서비스(결제 시스템, 회계 시스템)와 외부 서비스(결제 서비스 제공업체) 간 조정 프로세스
- 시스템 간 결제 정보가 일치하는지 비동기적으로 확인
- 계략적인 규모 측정
- 하루 100만 건 트랜잭션을 처리 (10 TPS)
- 일반적인 데이터베이스로 별 문제 없이 처리 가능하므로 대역폭 대신 결제 트랜잭션의 정확한 처리에 초점을 맞추자
2단계: 계략적 설계안 제시 및 동의 구하기
대금 수신 흐름
- 결제 서비스(payment service)
- 사용자로부터 결제 이벤트를 수락하고 결제 프로세스를 조율한다.
- 일반적으로 제3자 제공업체(결제 서비스 공급자, PSP)를 이용해 결제를 처리한다.
- 결제 실행자 (payment executor)
- 결제 서비스 공급자, 즉 PSP를 통해 결제 주문 하나를 실행
- 하나의 결제에는 여러 결제 주문이 포함될 수 있다.
- 결제 서비스 공급자 (Payment Service Provider)
- A 계정에서 B 계정으로 돈을 옮기는 역할을 담당
- 카드 유형
- 카드사는 신용 카드 업무를 처리하는 조직이다.
- 비자, 마스터카드, 디스커버리 등
- 원장 (ledger)
- 결제 트랜잭션에 대한 금용 기록
- 원장 시스템은 웹사이트의 총 수익을 계산하거나 향후 수익을 예측하는 등 분석에서 중요한 역할을 한다.
- 지갑 (wallet)
- 판매자의 계정 잔액을 기록
- 특정 사용자의 결제 총 금액을 기록할 수도 있다.
- 사용자가 주문 버튼을 클릭해 결제 이벤트가 결제 서비스로 전송
- 결제 서비스는 결제 이벤트를 데이터베이스에 저장
- 결제 실행자는 결제 주문을 데이터베이스에 저장
- 결제 실행자가 외부 PSP를 호출하여 신용 카드 결제를 처리
- 결제 실행자가 결제를 성공적으로 처리하고 나면 결제 서비스는 지갑을 갱신하여 특정 판매자의 잔고를 기록
- 지갑 서버는 갱신된 잔고 정보를 데이터베이스에 저장
- 지갑 서비스가 판매자 잔고를 성공적으로 갱신하면 결제 서비스는 원장을 호출
- 원장 서비스는 새 원장 정보를 데이터베이스에 추가
graph TD
사용자((사용자))
결제서비스[결제 서비스]
DB[(데이터베이스)]
결제실행자[결제 실행자]
PSP[외부 PSP]
지갑[지갑 서비스]
원장[원장 서비스]
사용자 -->|1 결제 이벤트| 결제서비스
결제서비스 -->|2 결제 이벤트 저장| DB
결제실행자 -->|3 주문 저장| DB
결제실행자 -->|4 카드결제 요청| PSP
PSP -->|4-1 결제 처리 완료| 결제실행자
결제실행자 -->|4-2 결제 완료| 결제서비스
결제서비스 -->|5 잔고 갱신| 지갑
지갑 -->|6 잔고 정보 저장| DB
지갑 -->|7 갱신 완료| 결제서비스
결제서비스 -->|7-1 원장 갱신 요청| 원장
원장 -->|8 원장 정보 저장| DB
결제 서비스 API
- POST
/v1/payment
- 결제 이벤트를 실행하며 여러 결제 주문이 포함될 수 있다.
- 요청 매개변수
- 구매자 정보
- 결제 이벤트 ID
- 암호화된 카드 정보 또는 결제 토큰 (PSP마다 다른 값)
- 결제 주문 목록 [(판매자 정보, 대금, 통화 단위, 주문 ID)]
- 결제 실행자가 PSP에 결제 요청을 전송할 때 주문 ID는 멱등 키로 사용한다.
- GET
/v1/payments/{:id}
- 주문 ID가 가리키는 단일 결제 주문 실행 상태를 반환
결제 서비스 데이터 모델
- 결제 서비스에는 결제 이벤트(payment event)와 결제 주문(payment order)의 두 테이블이 필요하다.
- 결제 시스템용 저장소는 일반적으로 ACID를 지원하는 전통적인 관계형 DB를 사용한다.
- 결제 이벤트 테이블 스키마
checkout_id
, buyer_info
, seller_info
, credit_card_info
, is_payment_done
checkout_id
는 외래 키로 한 번의 결제 행위는 하나의 결제 이벤트를 만든다.
- 하나의 결제 이벤트에는 여러 결제 주문이 포함될 수 있다.
- 결제 주문 테이블 스키마
payment_order_id
, buyer_account
, amount
, currency
, checkout_id
, payment_order_status
, ledger_updated
, wallet_updated
- 결제 주문 로직이 실행되며
payment_order_status
를 최종적으로 SUCCESS
처리
- 결제 성공 후 결제 서비스는 지갑 서비스를 호출해 판매자 잔액을 업데이트하고
wallet_updated
를 TRUE
로 변경
- 그리고 결제 서비스는 원장 서비스를 호출하여
ledger_updated
필드를 TRUE
로 갱신
- 동일한
checkout_id
아래 모든 결제 주문이 성공적으로 처리되면 결제 서비스는 is_payment_done
을 TRUE
로 업데이트한다.
- 아직 종결되지 않는 결제 주문을 모니터링 하기 위해 주기적으로 scheduled job을 통해 엔지니어에게 경고를 보내야 한다.
복식부기 원장 시스템
- 원장 시스템에는 복식부기(double-entry)라는 중요한 설계 원칙이 있다.
- 모든 결제 거래를 두 개의 별도 원장 계좌에 같은 금액으로 기록
- 한 계좌에선 차감이 이루어지고 다른 계좌에는 입금이 이루어진다.
- 복식부기 시스템에서 모든 거래 항목의 합계는 0이어야 한다.
- 복식부기 시스템을 통해 자금의 흐름을 시작부터 끝까지 추적할 수 있으며 결제 주기 전반에 걸쳐 일관성을 보장할 수 있다.
외부 결제 페이지
- 대부분 기업은 신용 카드 정보를 내부에 저장하지 않는다.
- 미국의 PCI DSS(Payment Card Industry Data Security Standard) 같은 복잡한 규정을 준수해야 하기 때문
- 신용 카드 정보를 취급하지 않기 위해 PSP에서 제공하는 외부 페이지를 사용한다.
- 우리 결제 서비스가 아닌 PSP가 제공하는 외부 결제 페이지가 직접 고객 카드 정보를 수집
대금 정산 흐름
- 대금 수신에선 PSP를 사용하여 구매자의 신용 카드에서 전자상거래 웹사이트 은행 계좌로 돈을 이체
- 대금 정산에선 타사 정산 서비스를 이용해 전자상거래 웹사이트 은행 계쫘에서 판매자 은행 계좌로 돈을 이체
- 일반적으로 정산 또한 외상 매입금 지급 서비스 제공업체를 이용한다.
3단계: 상세 설계
- 분산 시스템에서 오류와 장애는 피할 수 없다.
- 중복 결제? 네트워크 오류로 인한 결제 실패?
PSP 연동
- 대부분의 회사는 카드 시스템에 직접 연결하는 번거로움을 쏟지 않고 다음 두 방법 중 하나로 결제를 PSP와 연동한다.
- 민감한 결제 정보를 저장할 수 있다면 API를 통해 PSP와 연동
- 회사는 결제 웹페이지를 개발하고 카드를 수집
- PSP는 은행 연결, 다양한 카드를 지원하는 역할
- 민감한 결제 정보를 저장하지 않기로 결정한 경우
- PSP는 카드 결제 정보를 수집하고 외부 결제 페이지를 제공
graph LR
subgraph PSP[PSP]
C[PSP API]
end
subgraph 클라이언트_브라우저[Client Browser]
A[구매 페이지]
E[결제 페이지]
F[결제 완료 페이지]
end
subgraph 결제_시스템[결제 시스템]
B[결제 서비스]
D[(DB)]
end
A -->|1 구매 요청| B
B <-->|2 결제 생성 및 결제 토큰 반환| C
B -->|3 토큰 저장| D
B -->|4 PSP 결제 페이지 표시 | E
E -->|5 결제 시작 요청| C
C -->|6 결제 결과 반환| E
E -->|7 완표 페이지로 리다이렉트| F
C -->|8 웹훅 전송 최종 결과| B
- 사용자가 클라이언트 브라우저에서 ‘결제’ 버튼 클릭하면 클라이언트는 결제 서비스를 호출
- 결제 주문 정보를 수신한 결제 서비스는 결제 등록 요청을 PSP로 전송하고 결제 토큰을 반환 받는다.
- 결제 금액, 통화, 결제 요청 만료일 등을 포함하여 PSP에 요청
- 결제 주문이 정확히 한 번만 등록되도록 UUID 필드를 둔다. (주문 번호)
- 결제 토큰은 PSP의 UUID이다.
- 결제 서비스는 결제 토큰을 DB에 저장
- 클라이언트는 PSP가 제공하는 외부 결제 페이지를 표시한다.
- 외부 결제 페이지는 민감한 결제 정보를 수집하는데 우리 시스템에는 해당 데이터가 절대 넘어오지 않는다.
- 결제를 시작하면 PSP가 결제 처리를 시작한다.
- PSP가 결제 상태를 반환한다.
- 결제를 하는 사용자는 리다이렉션 되어 완료 페이지로 보내진다.
- PSP의 웹훅을 통해 결제 서비스는 결제 이벤트를 수신한다.
조정
- 시스템 구성 요소가 비동기적으로 통신하는 경우 응답이 반환된다는 보장이 없다.
- 결제 시스템은 성능 때문에 비동기 통신을 자주 한다.
- 비동기 통신을 하며 정확성을 보장하려면 조정이 필요하다.
- 관련 서비스 간 상태를 주기적으로 비교하여 일치하는지 확인하는 방법
- 은행이나 PSP는 매일 밤 모든 거래 내역이 기재된 정산 파일을 고객에게 전송한다.
- 조정 시스템은 정산 파일의 세부 정보를 읽어 원장 시스템과 비교한다.
- 조정은 결제 시스템 내부 일관성을 확인할 때도 사용되는데 발견될 수 있는 불일치는 다음 세 가지 범주가 존재한다.
- 어떤 유형의 문제인지 알고 있으며 해결 절차를 자동화할 수 있는 경우
- 어떤 유형의 문제인지는 알지만 해결을 자동화할 수 없는 경우
- 발생한 불일치 문제를 작업 대기열에 넣고 재무팀에서 수동 조정하는 수밖에 없다.
- 분류할 수 없는 유형의 문제인 경우
- 특별 작업 대기열에 넣고 재무팀에서 조사하도록 한다.
결제 지연 처리
- 대부분의 경우 결제는 몇 초 만에 처리되지만 완료되거나 거부되기까지 오래 걸리는 경우도 있다.
- PSP가 결제 요청의 위험성이 높다고 판단되어 담당자 검토를 요구하는 경우
- 신용 카드사가 구매 확인 용도로 카드 소유자 추가 정보를 요청하는 3D 보안 인증 같은 추가 보호 장치를 요구하는 경우
- 결제 서비스는 이런 요청도 처리할 수 있어야 하는데 이런 경우 PSP는 다음과 같이 처리한다.
- PSP는 결제가 대기 상태임을 알리는 상태 정보를 클라이언트에 반환하고 고객이 현재 결제 상태를 확인할 수 있는 페이지도 제공한다.
- PSP는 우리 회사를 대신해 대기 중인 결제 진행 상황을 추적하고 상태가 바뀌면 PSP에 등록된 웹훅을 통해 결제 서비스에 알린다.
내부 서비스 간 커뮤니케이션
- 동기식 패턴
- 소규모 시스템에선 잘 동작해지면 규모가 커지면 단점이 분명해진다.
- 성능 저하: 요청 처리에 관계된 서비스 하나에 발생한 성능 문제가 전체 시스템에 영향
- 장애 격리 곤란: PSP등의 서비스에 장애가 발생하면 클라이언트는 더 이상 응답을 받지 못한다.
- 높은 결합도: 요청 발신자는 수신자를 알아야만 한다.
- 낮은 확장성: 큐를 버퍼로 사용하지 않고서는 갑작스런 트래픽 증가에 대응하도록 확장이 어렵다.
- 비동기식 패턴
- 단일 수신자
- 각 요청(메시지)은 하나의 수신자 또는 서비스가 처리한다.
- 큐에는 복수의 구독자가 있을 수 있다.
- 처리된 메시지는 큐에서 바로 제거된다.
- 다중 수신자
- 각 요청(메시지)은 여러 수신자 또는 서버가 처리한다.
- 소비자가 수신한 메시지는 바로 사라지지 않는다.
- 동일한 메시지를 여러 서비스가 받아 처리할 수 있다.
- 즉 결제 시스템에 적합한데, 하나의 요청이 알림 전송, 재무 업데이트, 분석 결과 업데이트 등 다양한 용도에 쓰일 수 있기 때문이다.
- 일반적으로 동기식 통신은 설계는 쉽지만 서비스 자율성을 높이기에는 적합하지 않다.
- 비동기 통신은 설계는 복잡하지만 확장성 및 장애 감내 능력이 좋다.
결제 실패 처리
- 결제 상태 추적
- 결제 주기 모든 단계에서 결제 상태를 정확하게 유지해야 한다.
- 실패 발생 시 현 상태를 파악하고 재시도 또는 환불 여부를 결정한다.
- 결제 상태는 데이터 추가만 가능한 데이터베이스 테이블에 보관한다.
- 재시도 큐 및 실패 메시지 큐
- 재시도 큐: 일시적 오류 같은 재시도 가능 오류를 보내는 큐
- 실패 메시지 큐: 반복적으로 실패한 메시지를 보낸다.
정확히 한 번 전달
- 결제 주문은 정확히 한 번만 실행되도록 시스템을 설계해야 한다.
- 이 문제는 두 부분으로 나누면 훨씬 쉽게 해결 가능하다.
- 최소 한 번은 실행된다.
- 최대 한 번 실행된다.
- 재시도
- 네트워크 오류 등으로 결재를 다시 시도해야 하는 경우 최소 한 번 실행되도록 보장할 수 있다.
- 재시도 메커니즘엔 재시도 간격을 정하는 것이 중요하다.
- 즉시 재시도: 클라이언트가 즉시 요청을 보냄
- 고정 간격: 재시도 전 일정 시간 대기
- 증분 간격: 재시도 전에 기다리는 시간을 특정 양 만큼 점진적으로 늘리기
- 지수적 백오프: 재시도 전 기다리는 시간을 직전 대비 두 배씩 늘리는 방안
- 취소: 요청을 철회
- 모든 상황에 맞는 해결책은 없지만 일반적으로 네트워크 문제가 단시간 내 해결될 것 같지 않다면 지수적 백오프를 사용하는 편이 좋다.
- 멱등성
- 여러 오류로 클라이언트가 결제 버튼을 두 번 클릭하거나 두번 요청이 오는 경우 최대 한 번 실행을 보장하기 위해 멱등성을 보장해야 한다.
- 일반적으로 결제에선 멱등 키로 장바구니 ID 등을 사용하는데 중복된 멱등 키로 온 요청에 대해선 이전 결제 요청의 가장 최근 상태를 반환하게 한다.
- 또 다른 방법은 데이터베이스의 고유 키 제약 조건을 활용하는 것인데 멱등 키가 두 번째로 저장되려고 하는 순간 유일성 예외가 터져 이중 결제를 막아줄 것이다.
일관성
- 결제 실행 과정에서 상태 정보를 유지 관리하는 여러 서비스가 호출된다.
- 분산 환경에서 발생할 수 있는 데이터 불일치를 해결하려면 요청이 ‘정확히 한 번 처리’되도록 보장하는 것이 중요하다.
- 일관성 유지를 위해 일반적으로 멱등성과 조정 프로세스를 활용한다.
- 데이터를 다중화하는 경우 복제 지연으로 인해 원본과 사본의 불일치가 발생할 수 있다.
- 주 데이터베이스에서만 읽기와 쓰기 연산을 처리하는 방법이 있지만 확장성이 떨어지고 사본은 트래픽을 처리하지 않으므로 자원이 낭비된다.
- 모든 사본이 항상 동기화되도록 합의 알고리즘을 사용하거나 합의 기반 분산 데이터베이스를 사용하는 방법도 있다.
결제 보안
- 요청/응답 도청 → HTTPS 사용
- 데이터 변조 → 암호화 및 무결성 강화 모니터링
- 중간자 공격 → 인증서 고정과 함께 SSL 사용
- 데이터 손실 → 여러 지역에 걸쳐 DB 복제 및 스냅숏 생성
- DDoS → 처리율 제한 및 방화벽
- 카드 도난 → 실제 카드 번호를 사용하는 대신 토큰을 결제에 사용
- PCI 규정 준수 → PCI DSS는 브랜드 신용 카드를 처리하는 조직을 위한 정보 보안 표준이다.
- 사기(fruad) → 주소 확인, 카드 확인번호(CVV), 사용자 행동 분석 등