11장. 결제 시스템
11장 요약

- 이번 장에서는 결제 시스템을 설계한다.
- 최근 몇 년 동안 전세계적으로 전자상거래 인기는 폭발적으로 증가했다.
- 전자상거래를 가능하게 하는 것은 바로 결제 시스템이다.
- 안정적이고 확장 가능하며 유연한 결제 시스템은 필수다.
- 결제시스템이란?
"금전적 가치의 이전을 통해 금융 거래를 정산하는데 사용되는 모든 시스템"
여기에는 가치 교환을 가능하게 하는 제도, 도구, 사람, 규칙, 절차, 표준, 기술이 포함된다.
- 결제 시스템은 얼핏 보기엔 이해하기 쉬우나, 작업하기 부담스러운 시스템이기도 하다. 작은 실수로도 상당한 매출 손실이 발생하며 사용자 믿음이 무너질 수 있기 때문이다.
1단계: 문제 이해 및 설계 범위 확정
- 사람마다 결제 시스템에 대한 생각은 다르다.
- 어떤 사람은 애플 페이, 구글 페이 같은 디지털 지갑을 생각하고,
어떤 사람은 페이팔, 스트라이프 같은 결제 처리 백엔드 시스템을 생각한다.
- 따라서 면접을 시작할 때 정확한 요구사항을 파악해야 한다.
👩🏻💼(지원자) : 어떤 결제 시스템을 만들어야 하나요?
👨🏻💻(면접관) : 아마존닷컴 같은 전자상거래 애플리케이션을 위한 결제 백엔드를 구축한다고 가정합시다. 고객이 아마존에서 주문을 하면 결제 시스템은 돈의 흐름에 대한 모든 것을 처리해야 합니다.
👩🏻💼(지원자) : 어떤 결제 방법을 지원해야 하나요? 신용 카드, 페이팔, 은행 카드?
👨🏻💻(면접관) : 결제 시스템은 실생활에서 사용 가능한 모든 옵션을 지원해야 합니다. 하지만 이번 면접에서는 신용 카드 결제만 처리해봅시다.
👩🏻💼(지원자) : 신용 카드 결제 처리를 직접 해야 하나요?
👨🏻💻(면접관) : 아닙니다. 스트라이프, 브레인트리, 스퀘어 같은 전문 결제 서비스 업체를 사용합시다.
👩🏻💼(지원자) : 신용 카드 데이터를 시스템에 저장해야 하나요?
👨🏻💻(면접관) : 보안 및 법규 준수에 대한 요건이 아주 까다로운 관계로, 카드 번호를 시스템에 직접 저장하지는 않을 것입니다. 민감한 신용 카드 데이터 처리는 결제 처리 업체에 의존합니다.
👩🏻💼(지원자) : 전 세계를 대상으로 해야 하나요? 다양한 통화 및 국제 결제를 지원해야 합니까?
👨🏻💻(면접관) : 좋은 질문입니다. 예, 전 세계적으로 사용될 수 있는 애플리케이션이지만 이번 면접 동안엔 하나의 통화만 사용한다고 가정하시죠.
👩🏻💼(지원자) : 하루에 몇 건의 결제가 이루어지나요?
👨🏻💻(면접관) : 하루 100만건의 거래가 이루어진다고 하겠습니다.
👩🏻💼(지원자) : 아마존과 같은 전자상거래 사이트에서 매월 판매자에게 대금을 지급하는 절차를 지원해야 하나요?
👨🏻💻(면접관) : 예.
👩🏻💼(지원자) : 요구사항을 다 파악한 것 같은데요. 다른 주의사항이 있을까요?
👨🏻💻(면접관) : 네. 결제 시스템은 많은 내부 서비스(계정, 분석 등) 및 외부 서비스(결제 서비스 공급자)와 연동합니다. 한 서비스에 장애가 발생하면 서비스 간 상태가 달라지는 일이 벌어질 수 있는데요. 따라서 조정 작업을 수행하고 불일치하는 부분이 발견되면 교정해야 합니다. 이것도 필수 요건이죠.
기능 요구사항
- 대금 수신(pay-in) 흐름 : 결제 시스템이 판매자를 대신해 고객으로부터 대금을 수령한다.
- 대금 정산(pay-out) 흐름 : 결제 시스템이 전 세계의 판매자에게 제품 판매 대금을 송금한다.
비기능 요구사항
- 신뢰성 및 내결함성 : 결제 실패는 신중하게 처리해야 한다.
내결함성=고장나도 멈추지 않는 시스템
- 내부 서비스(결제 시스템, 회계 시스템)와 외부 서비스(결제 서비스 제공업체)간의 조정 프로세스 : 시스템 간 결제 정보가 일치하는지 비동기적으로 확인한다.
개략적인 규모 추정
- 이 시스템은 하루에 100만건의 트랜잭션을 처리해야 하는데,
- 1,000,000 건의 트랜잭션 / 10^5 = 초당 10건의 트랜잭션(TPS)이다.
- 10TPS는 일반적인 데이터베이스로 별 문제 없이 처리 가능한 양이므로, 처리 대역폭 대신 결제 트랜잭션의 정확한 처리에 초점을 맞춰 면접을 진행해야 한다.
2단계: 개략적 설계안 제시 및 동의 구하기

- 결제 흐름은 자금의 흐름을 반영하기 위해 크게 두 단계로 세분화 된다.
- 전자상거래 사이트 아마존을 예로 들어보자. 구매자가 주문을 하면 아마존의 은행 계좌로 돈이 들어오는데, 이것이 바로 대금 수신 흐름이다.
- 이 돈은 아마존의 은행 계좌에 있지만, 소유권이 전부 아마존에 있는 것은 아니다. 판매자가 상당 부분을 소유하며, 아마존은 수수료를 받고 자금 관리자 역할만 수행한다.
- 나중에 제품이 배송되고 나면, 그때까지 계좌에 묶여 있던 판매 대금에서 수수료를 제외한 잔액이 판매자의 은행 계좌로 지급된다. 이것이 대금 정산 흐름이다.
대금 수신 흐름

- 결제 서비스
- 사용자로부터 결제 이벤트를 수락하고 결제 프로세스를 조율한다.
- 일반적으로 가장 먼저 하는 일은 AML/CFT 같은 규정을 준수하는지, 자금 세탁이나 테러 자금 조달 같은 범죄 행위의 증거가 있는지 평가하는 위험 점검(risk check)이다.
- 결제 서비스는 이 위험 확인을 통과한 결제만 처리한다.
- 일반적으로 위험 확인 서비스는 매우 복잡하고 고도로 전문화되어 있어서 제3자 제공업체를 이용한다.
- 결제 실행자
- 결제 실행자는 결제 서비스 공급자(PSP)를 통해 결제 주문(payment order) 하나를 실행한다.
- 하나의 결제 이벤트에는 여러 결제 주문이 포함될 수 있다.
- 결제 서비스 공급자
- PSP(Payment Service Provider)는 A계정에서 B계정으로 돈을 옮기는 역할을 담당한다. 본 예제에선 구매자의 신용카드 계좌에서 돈을 인출하는 역할을 담당한다.
- 카드 유형
- 카드사는 신용 카드 업무를 처리하는 조직이다.
- 잘 알려진 카드 유형으로는 비자, 마스터카드, 디스커버리 등이 있다.
(카드 생태계는 매우 복잡하다)
- 원장(ledger)
- 원장은 결제 트랜잭션에 대한 금융 기록이다.
- 예를 들어, 사용자가 판매자에게 1달러를 결제하면 사용자로부터 1달러를 인출하고 판매자에게 1달러를 지급하는 기록을 남긴다.
- 원장 시스템은 전자상거래 웹사이트의 총 수익을 계산하거나, 향후 수익을 예측하는 등, 결제 후 분석에서 매우 중요한 역할을 한다.
- 지갑(wallet)
- 지갑에는 판매자의 계정 잔액을 기록한다.
- 특정 사용자가 결제한 총 금액을 기록할 수도 있다.
일반적인 결제 흐름
- 사용자가 '주문하기' 버튼 클릭하면 결제 이벤트가 생성되어 결제 서비스로 전송된다.
- 결제 서비스는 결제 이벤트를 데이터베이스에 저장한다.
- 때로는 단일 결제 이벤트에 여러 결제 주문이 포함될 수 있다. 한 번 결제로 여러 판매자의 제품을 처리하는 경우가 그 예다. 전자상거래 웹사이트에서 한 결제를 여러 결제 주문으로 분할하는 경우, 결제 서비스는 결제 주문마다 결제 실행자를 호출한다.
- 결제 실행자는 결제 주문을 데이터베이스에 저장한다.
- 결제 실행자가 외부 PSP를 호출하여 신용 카드 결제를 처리한다.
- 결제 실행자가 결제를 성공적으로 처리하고 나면, 결제 서비스는 지갑을 갱신해서 특정 판매자의 잔고를 기록한다.
- 지갑 서버는 갱신된 잔고 정보를 데이터베이스에 저장한다.
- 지갑 서비스가 판매자 잔고를 성공적으로 갱신하면, 결제 서비스는 원장을 호출한다.
- 원장 서비스는 새 원장 정보를 데이터베이스에 추가한다.
결제 서비스 API
- POST /v1/payments : 결제 실행
- 이 엔드포인트는 결제 이벤트를 실행한다.
- 결제 이벤트에는 여러 결제 주문이 포함될 수 있다.

- buyer_info (json) : 구매자 정보
- checkout_id (string) : 해당 결제 이벤트를 식별하는 전역적으로 고유한 ID
- credit_card_info (json) : 암호화된 신용 카드 정보 or 결제 토큰. PSP마다 다른 값.
- payment_orders (list) : 결제 주문 목록

- seller_account : 대금 수령할 판매자
- amount (string) : 해당 주문으로 전송되어야 할 대금
- currency : 통화 단위
- payment_order_id : 해당 주문을 식별하는 전역적으로 고유한 ID
- 결제 실행자가 타사 PSP에 결제 요청을 전송할 때,
PSP는 payment_order_id를 중복제거 ID로 사용한다. 멱등키 라고도 한다.
- amount가 double이 아닌 string 타입인 것에 유의하자.
- 프로토콜, 소프트웨어, 하드웨어에 따라 직렬화/역직렬화에 사용하는 숫자 정밀도가 다를 수 있다. 이러한 차이가 의도치 않은 반올림 오류를 유발할 수 있다.
- 숫자가 매우 클수도 있고, 매우 작을수도 있다.
- 따라서 전송 및 저장 시 숫자는 문자열로 보관하는 것이 좋다. 표시하거나 계산할 때만 숫자로 변환한다.
- GET /v1/payments/{:id} : 결제 상태 조회
- payment_order_id가 가리키는 단일 결제 주문의 실행 상태를 반환한다.
- 이 결제 API는 잘 알려진 일부 PSP의 API와 유사하다.
결제 서비스 데이터 모델
- 결제 서비스에는 결제 이벤트(payment event)와 결제 주문(payment order)의 두개 테이블이 필요하다.
- 결제 시스템용 저장소 솔루션을 고를 때 일반적으로 성능은 가장 중요한 고려사항은 아니다. 대신 다음 사항에 중점을 둔다.
- 1. 안정성이 검증 되었는가?
다른 대형 금융 회사에서 수년 동안 긍정적 피드백을 받으며 사용된 적 있는가?
- 2. 모니터링 및 데이터 탐사에 필요한 도구가 풍부하게 지원되는가?
- 데이터베이스 관리자(DBA) 채용 시장이 성숙했는가?
숙련된 DBA를 쉽게 채용할 수 있는가? (아주 중요한 요소다)
- 일반적으로 NoSQL 보단 ACID 트랜잭션을 지원하는 전통적 관계형 데이터베이스를 선호한다.
- 결제 이벤트 테이블에는 자세한 결제 이벤트 정보가 저장된다.
테이블 스키마
결제 이벤트

결제 주문

checkout_id : 외래키. 한 번의 결제 행위는 하나의 결제 이벤트를 만들고, 하나의 결제 이벤트에는 여러개의 결제 주문이 포함될 수 있다.
- 구매자의 신용 카드에서 금액을 공제하기 위해 타사 PSP를 호출하면 판매자 대신 전자상거래 웹사이트의 은행 계좌에 이체가 이루어지는데, 이 프로세스를 대금 수신(pay-in)이라고 부른다.
- 제품이 배송되는 등 대금 정산 조건이 충족되면 해당 대금을 판매자에게 정산하는 절차를 시작한다.
- 그 결과로 전자상거래 웹사이트 은행 계좌에서 판매자의 은행 계좌로 금액이 이체된다.
- 따라서 사용자의 결제를 처리하는 중 판매자의 은행 계좌가 아닌 구매자의 카드 정보만 필요하다.
payment_order_status : 결제 주문 실행 상태를 유지하는 Enum 타입이다.
NOT_STARTED, EXECUTING, SUCCESS, FAILED
- 업데이트 로직
- payment_order_status 초기값은 NOT_STARTED
- 결제 서비스는 결제 실행자에게 주문을 전송하면 EXECUTING으로 바꾼다.
- 결제 서비스는 결제 처리자의 응답에 따라 payment_order_status값을 SUCCESS 또는 FAIL 로 변경한다.
- payment_order_status값이 SUCCESS로 결정되면 결제 서비스는 지갑 서비스를 호출해서 판매자 잔액을 업데이트하고, wallet_updated 필드를 true로 업데이트한다. 여기선 지갑 업데이트가 항상 성공한다고 가정한다.
- 이 절차가 끝나면 결제 서비스는 다음 단계로 원장 서비스를 호출하여 원장 데이터베이스의 ledger_updated 필드를 true로 갱신한다.
- 동일한 checkout_id 아래 모든 결제 주문이 성공적으로 처리되면 결제 서비스는 결제 이벤트 테이블의 is_payment_done을 TRUE로 업데이트 한다.
- 일반적으로, 종결되지 않은 결제 주문을 모니터링 하기 위해 주기적으로 실행되는 작업(scheduled job)을 마련해둔다.
- 이 작업은 임계값 형태로 설정된 기간이 지나도록 완료되지 않은 결제 주문이 있을 경우 살펴보도록 엔지니어에게 경보를 보낸다.
복식부기 원장 시스템
- 원장 시스템에는 복식부기(double-entry)라는 아주 중요한 설계 원칙이 있다. (복식부기 회계/부기(bookeeping) 라고도 함)
- 복식부기는 모든 결제 시스템에 필수 요소이며 정확한 기록을 남기는 데 핵심적 역할을 한다.
- 모든 결제 거래를 두개의 별도 원장 계좌에 같은 금액으로 기록한다. 한 계좌에서는 차감, 다른 계좌에서는 입금이 이루어진다.
- 복식부기 시스템에서 모든 거래 항목의 합계는 0이어야 한다.
- 이 시스템을 활용하면 자금의 흐름을 시작부터 끝까지 추적할 수 있으며, 결제 주기 전반에 걸쳐 일관성을 보장할 수 있다.
외부 결제 페이지
- 대부분의 기업은 신용카드 정보를 내부에 저장하지 않는다. (복잡한 규정이 있음)
- 신용카드정보를 취급하지 않기 위해 기업들은 PSP에서 제공하는 외부 신용카드 페이지를 사용한다.
- 웹 사이트의 경우 이 외부 신용 카드 페이지는 위젯 또는 iframe 이며,
모바일 애플리케이션의 경우엔 결제 SDK에 포함된 사전에 구현된 페이지다.
- 여기서 중요한 점은, 우리 결제 서비스가 아니라 PSP가 제공하는 외부 결제 페이지가 직접 고객 카드 정보를 수집한다는 것이다.
대금 정산 흐름
- 대금 정산(pay-out)흐름의 구성 요소는 대금 수신 흐름과 아주 유사하다.
- 한가지 차이는 PSP를 사용해서 구매자 신용카드에서 전자상거래 웹사이트 은행 계좌로 돈을 이체하는 대신, 정산 흐름에서는 타사 정산 서비스를 사용해서 전자상거래 웹사이트 은행 계좌에서 판매자 은행 계좌로 돈을 이체한다는 점이다.
- 일반적으로 결제 시스템은 대금 정산을 위해 티팔티 같은 외상매입금 지급 서비스 제공업체를 이용한다. (대금 정산에도 다양한 부기 및 규제 요구사항이 있음)
3단계: 상세 설계
- 시스템을 더 빠르고 강력하며 안전하게 만드는 데 초점을 맞춘다.
- 분산 시스템에서 오류와 장애는 피할 수 없을 뿐만 아니라 흔한 일이다.
- 예를 들어, 고객이 '결제' 버튼을 여러번 누르면 어떻게 될까?
여러번 요금이 청구되나? 네트워크 연결 불량으로 인한 결제 실패는 어떻게 처리해야 하나?
- PSP 연동
- 조정(coordination)
- 결제 지연 처리
- 내부 서비스 간 통신
- 결제 실패 처리
- '정확히 한 번(exact-once)' 전달
- 일관성
- 보안
PSP 연동
- 결제 시스템이 은행이나 비자/마스터카드 같은 카드 시스템에 직접 연결할 수 있다면 PSP 없이도 결제할 수 있다.
- 하지만 그런 직접 연결은 아주 특수한 경우로 한정된다.
- 대부분의 회사는 다음 2가지 방법 중 하나로 결제 시스템을 PSP와 연동한다.
- 회사가 민감한 결제 정보를 안전하게 저장할 수 있다면 API를 통해 PSP와 연동하는 방법을 택할 수 있다.
- 회사는 결제 웹페이지를 개발하고 민감한 결제 정보를 수집하며, PSP는 은행 연결, 다양한 카드 유형을 지원하는 역할을 한다.
- 복잡한 규정 및 보안 문제로 인해 민감한 결제 정보를 저장하지 않기로 결정한 경우, PSP는 카드 결제 세부 정보를 수집해서 PSP에 안전하게 저장할 수 있도록 외부 결제 페이지를 제공한다. (대부분의 기업이 택하는 접근법)
외부 결제 페이지 이용 흐름

- 사용자가 클라이언트 브라우저에서 '결제' 버튼을 클릭한다. 클라이언트는 결제 주문 정보를 담아 결제 서비스를 호출한다.
- 결제 주문 정보를 수신한 결제 서비스는, 결제 등록 요청을 PSP로 전송한다.
- 이 등록 요청에는 결재 금액, 통화, 결제 요청 만료일, 리디렉션 URL 등의 결제 정보가 포함된다.
- 결제주문이 정확히 한 번만 등록될 수 있도록, UUID 필드를 둔다.
- 이 UUID는 비중복 난수라고도 부른다. 일반적으로 결제주문 ID로 사용된다.
- PSP는 결제 서비스에 토큰을 반환한다. (결제 토큰)
- 토큰은 등록된 결제 요청을 유일하게 식별하는 PSP가 발급한 UUID다.
- 나중에 이 토큰을 사용해서 결제 등록 및 결제 실행 상태를 확인할 수 있다.
- 결제 서비스는 PSP가 제공하는 외부 결제 페이지를 호출하기 전에 토큰을 데이터베이스에 저장한다. (DB에 일단 토큰 저장)
- 토큰을 저장하고 나면 클라이언트는 PSP가 제공하는 외부 결제 페이지를 표시한다.
- 모바일 애플리케이션은 일반적으로 이를 위해 PSP SDK를 연동한다.
- 여기선 스트라이프 사의 웹 연동 사례를 들겠다.
- 스트라이프가 제공하는 자바스크립트 라이브러리에는 결제 UI를 표시하고, 민감한 결제 정보를 수집하고, 결제를 완료하는 등의 작업을 위해 PSP를 직접 호출하는 로직이 포함되어 있다.
- 민감한 결제정보는 스트라이프가 수집하며, 이런 정보는 우리 시스템으로는 절대 넘어오지 않는다.
- 외부 결제 페이지는 일반적으로 다음 두가지 정보가 필요하다.
- 4단계에서 받은 토큰 : PSP의 자바스크립트 코드는 이 토큰을 사용해서 PSP의 백엔드에서 결제 요청에 대한 상세 정보를 검색한다. 이 과정을 통해 알아내야 하는 중요 정보는, 사용자에게 받을 금액이다!
- 리디렉션 URL : 결제 완료 후 호출될 웹페이지의 URL이다. PSP의 자바스크립트는 결제가 완료되면 브라우저를 리디렉션 URL로 돌려보낸다.
- 사용자는 신용카드번호, 소유자이름, 카드 유효기간등 결제 세부 정보를 PSP 웹 페이지에 입력한다음 결제 버튼을 클릭한다. PSP가 결제 처리를 시작한다.
- PSP가 결제 상태를 반환한다.
- 이제 사용자는 리디렉션 URL이 가리키는 웹페이지로 보내진다. 이때 보통 7단계에서 수신된 결제상태(payResult)가 URL에 추가된다.
https://your-company.com/?tokenID+JIQU123F&payResult=X324FSa 같은 형태로.
- 비동기적으로 PSP는 웹훅을 통해 결제 상태와 함께 결제 서비스를 호출한다. 웹훅은 결제 시스템 측에서 PSP를 처음 설정할 때 등록한 URL이다.
결제 시스템이 웹훅을 통해 결제 이벤트를 다시 수신하면 결제 상태를 추출해서 결제 주문 데이터베이스 테이블의 payment_order_status 필드를 최신상태로 업데이트한다.
- 외부 결제 페이지가 잘 동작할 때 시스템들이 어떻게 상호 연동하는지 설명했는데, 실제로 위 아홉단계 각각이 네트워크 문제로 실패할 수 있다.
- 장애가 발생하면 체계적으로 처리할 수 있는 방법이 있을까? 조정(reconciliation)이 바로 그 방법이다!
조정(coordination)

- 시스템 구성 요소가 비동기적으로 통신하는 경우 메시지가 전달되거나 응답이 반환된다는 보장이 없다.
- 이는 시스템 성능을 위해 비동기 통신을 자주 사용하는 결제 관련 사업에 일반적인 문제다.
- PSP나 은행 같은 외부 시스템도 비동기 통신을 선호한다.
- 그렇다면 어떻게 정확성을 보장할 수 있을까? → 정답은 "조정"이다.
- 관련 서비스 간 상태를 주기적으로 비교해서 일치하는지 확인하는 것이다.
- 일반적으로 결제 시스템의 마지막 방어선으로 받아들여 진다.
- 매일 밤 PSP나 은행은 고객에게 정산 파일을 보낸다.
- 정산 파일에는 은행 계좌의 잔액과 하루 동안 해당 계좌에서 발생한 모든 거래내역이 기재되어 있다.
- 조정 시스템은 정산 파일의 세부정보를 읽어 원장 시스템과 비교한다.
- 조정은 결제 시스템의 내부 일관성을 확인할 때도 사용된다.
예를 들어, 원장과 지갑의 상태가 같은지 확인할 수 있다.
- 조정 중 발견된 차이는 일반적으로 재무팀에 의뢰해서 수동으로 고친다.
발생 가능한 불일치 문제 및 해결 방안
- 어떤 유형의 문제인지 알고 있고, 문제 해결 절차를 자동화 할 수 있는 경우 :
원인과 해결 방법을 알고 있으며, 자동화 프로그램 작성이 비용 효율적인 경우다. 엔지니어는 발생한 불일치 문제의 분류와 조정 작업을 모두 자동화 할 수 있다.
- 어떤 유형의 문제인지는 알지만 문제 해결 절차를 자동화 할 수 없는 경우 : 수동으로 수정
- 분류할 수 없는 유형의 문제인 경우 : 불일치가 어떻게 발생하였는지 알지 못하는 경우.
결제 지연 처리
- 결제 요청은 많은 컴포넌트를 거치며, 내부 및 외부의 다양한 처리 주체와 연동한다. 대부분의 경우 결제 요청은 몇 초 만에 처리되지만, 완료되거나 거부되기까지 몇 시간 또는 며칠이 걸리는 경우도 있다.
- 다음은 결제 요청이 평소보다 오래 걸리게 되는 몇가지 사례다.
- PSP가 해당 결제 요청의 위험성이 높다고 보고 담당자 검토를 요구하는 경우
- 신용카드사가 구매확인 용도로 카드 소유자의 추가 정보를 요청하는 #D 보안 인증 같은 추가 보호 장치를 요구하는 경우
- 결제 서비스는 처리하는데 시간이 오래 걸리는 이런 요청도 처리할 수 있어야 한다.
- 구매 페이지가 외부 PSP에 호스팅 되는 경우(일반적인 관행) PSP는 다음과 같이 처리한다.
- PSP는 결제가 대기(pending) 상태임을 알리는 상태 정보를 클라이언트에 반환하고, 클라이언트는 이를 사용자에게 표시한다. 클라이언트는 또한 고객이 현재 결제 상태를 확인할 수 있는 페이지도 제공한다.
- PSP는 우리 회사를 대신해 대기중인 결제의 진행 상황을 추적하고, 상태가 바뀌면 PSP에 등록된 웹훅을 통해 결제 서비스에 알린다.
- 결제 요청이 최종적으로 완료되면 PSP는 방금 언급한 사전에 등록된 웹훅을 호출한다. 결제 서비스는 내부 시스템에 기록된 정보를 업데이트 하고 고객에게 배송을 완료한다.
- 어떤 PSP는 웹훅을 통해 결제 서비스에 결제 상태 변경을 알리는 대신, 결제 서비스로 하여금 대기 중인 결제 요청의 상태를 주기적으로 확인(polling)하도록 하기도 한다.
내부 서비스 간 통신
동기식 통신
- HTTP 같은 동기식 통신은 소규모 시스템에서는 잘 작동하지만, 규모가 커지면 단점이 분명해진다.
- 동기식 통신에서 한 요청에 응답을 만드는 처리 주기는 관련된 서비스가 많을수록 길어진다.
- 단점
- 성능 저하 : 요청 처리에 관계된 서비스 가운데 하나에 발생한 성능 문제가 전체 시스템의 성능에 영향을 미친다.
- 장애 격리 곤란: PSP 등 서비스에 장애가 발생하면 클라이언트는 더이상 응답을 받지 못한다.
- 높은 결합도: 요청 발신자는 수신자를 알아야만 한다.
- 낮은 확장성: 큐를 버퍼로 사용하지 않고선, 갑작스러운 트래픽 증가에 대응할 수 있도록 시스템 확장이 어렵다.
비동기 통신
- 단일 수신자: 각 요청(메시지)은 하나의 수신자 또는 서비스가 처리한다.
- 일반적으로 공유 메시지 큐를 사용해서 구현한다.
- 큐에는 복수의 구독자가 있을 수 있으나 처리된 메시지는 큐에서 바로 제거된다.

- 예를 들어, 서비스A/B 모두 같은 메시지 큐를 구독한다. 각각 m1, m2 메시지를 처리하면 두 메시지는 모두 큐에서 사라진다.
- 다중 수신자: 각 요청(메시지)은 여러 수신자 또는 서버가 처리한다.
- 카프카는 이런 시나리오를 잘 처리할 수 있다.
- 소비자가 수신한 메시지는 카프카에서 바로 사라지지 않는다.
- 그래서 동일 메시지를 여러 서비스가 받아 처리할 수 있다.
- 따라서 결제 시스템 구현에 적합한데, 하나의 요청이 푸시 알림 전송, 재무 보고 업데이트, 분석 결과 업데이트 등 다양한 용도에 쓰일 수 있기 때문이다.

- 일반적으로 동기식 통신은 설계하기 쉽지만 서비스 자율성을 높이기엔 적합하지 않다. 의존성 그래프가 커지면 전반적 성능은 낮아진다.
- 비동기 통신은 설계의 단순성, 데이터 일관성을 시스템 확장성 및 장애 감내 능력과 맞바꾼 결과다. 비즈니스 로직이 복잡하고 타사 서비스 의존성이 높은 대규모 결제 시스템에서는 비동기 통신이 더 나은 선택이다! ✅
결제 실패 처리
- 모든 결제 시스템은 실패한 결제를 적절히 처리할 수 있어야 한다.
- 안정성 및 결함 내성은 결제 시스템의 핵심적 요구사항이다.
처리 방법1. 결제 상태 추적
- 결제 주기의 모든 단계에서 결제 상태를 정확히 유지하는 것은 매우 중요하다.
- 실패가 일어날 때마다 결제 거래의 현재 상태를 파악하고, 재시도 또는 환불이 불가한지 여부를 결정한다.
- 결제 상태는 데이터 추가만 가능한(append-only) 데이터베이스 테이블에 보관한다.
처리 방법2. 재시도 큐 및 실패 메시지 큐 ⭐️
- 실패를 우하하게 처리하기 위해서는 재시도 큐와 실패 메시지 큐를 두는것이 바람직하다.
- 재시도 큐 : 일시적 오류 같은 재시도 가능 오류는 재시도 큐에 보낸다.
- 실패 메시지 큐 : 반복적으로 처리에 실패한 메시지는 결국에는 실패 메시지 큐로 보낸다. 이 큐는 문제가 있는 메시지를 디버깅하고, 격리해서 성공적으로 처리되지 않은 이유를 파악하기 위한 검사에 유용하다.

- 재시도 가능한지 확인
- a. 재시도 가능 실패는 재시도 큐로 보낸다.
- b. 잘못된 입력 같이 재시도 불가능한 실패는 오류 내역을 데이터베이스에 저장한다.
- 결제 시스템은 재시도 큐에 쌓인 이벤트를 읽어서 실패한 결제를 재시도한다.
- 결제 거래가 다시 실패하는 경우엔 다음과 같이 처리한다.
- a. 재시도 횟수가 임계값 이내라면, 해당 이벤트를 다시 재시도 큐로 보낸다.
- b. 재시도 횟수가 임계값을 넘으면 해당 이벤트를 실패 메시지 큐에 넣는다. 이런 이벤트에 대해선 별도 조사가 필요할 수 있다.
- 실무에서 이런 큐가 어떻게 쓰이는지 궁금하다면?
우버에서 카프카를 활용해 결제 시스템 안정성과 결함 내성 요건을 어떻게 충족하고 있는지를 참고해보자!
⭐️'정확히 한 번' 전달 = '최소 한번 실행' + '최대 한번 실행'
- 결제 시스템에 발생 가능한 가장 심각한 문제 중 하나는 고객에게 이중으로 청구하는 것이다.
- 결제 주문이 정확히 한번만 실행되도록 결제 시스템을 설계하는 것이 중요하다.
- 언뜻 보기엔 메시지를 정확히 한번 전달하는 것은, 매우 어려운 문제처럼 느껴지지만, 문제를 두부분으로 나누면 훨씬 쉽게 해결할 수 있다.
- 수학적으로 보자면, 다음의 요건이 충족되면 주어진 연산은 정확히 한번만 실행된다.
- 재시도를 통해 최소 한번 실행을 보증하는 방법과, 멱등성 검사를 통해 최대 한번 실행을 보증하는 방법을 알아보자!
'최소 한번 실행 보증 방법' → 재시도 ✅
- 네트워크 오류나 시간 초과로 인해 결제 거래를 다시 시도해야 하는 경우가 있다.
- 재시도 메커니즘을 활용하면 어떤 결제가 최소 한번은 실행되도록 보장 가능하다.✅
- 재시도 메커니즘을 도입할 땐, 얼마나 간격을 두고 재시도할지 정하는 것이 중요하다.
- 재시도 전략
- 즉시 재시도 : 클라이언트는 즉시 요청을 다시 보낸다.
- 고정 간격 : 재시도 전에 일정 시간 기다린다.
- 증분 간격 : 재시도 전에 기다리는 시간을 특정 양만큼 전진적으로 늘려 나간다.
- 지수적 백오프 : 재시도 전에 기다리는 시간을 직전 재시도 대비 두배씩 늘려 나가는 방안이다.
- 취소 : 요청을 철회하는 방안. 실패가 영구적이거나 재시도를 하더라도 성공 가능성이 낮은 경우에 흔히 사용되는 방안이다.
- 적절한 재시도 전략을 결정하는 것은 어렵다. 다만 일반적으로 적용 가능한 지침은, 네트워크 문제가 단시간 내 해결될 것 같지 않다면 지수적 백오프를 사용하라는 것이다.
- 지나치게 공격적인 재시도 전략은 컴퓨팅 자원을 낭비하고 서비스 과부하를 유발한다. 에러코드를 반환할 때는 Retry-After 헤더를 같이 붙여 보내는 것이 바람직하다.
- 재시도 시 발생할 수 있는 잠제적 문제는 이중결제다.
- 시나리오1: 결제 시스템이 외부 결제 페이지를 통해 PSP와 연동하는 환경에서 클라이언트가 결제 버튼을 두번 중복 클릭한다.
- 시나리오2: PSP가 결제를 성공적으로 하였으나, 네트워크 오류로 인해 응답이 결제 시스템에 도달하지 못했다!!! 사용자가 '결제'버튼을 다시 클릭하거나 클라이언트가 결제를 다시 시도한다.
- 이중 결제를 방지하려면 결제는 '최대 한번' 이루어져야 한다.
'최대 한번 실행'은 다른말로 == 멱등성 이라고 한다.
'최대 한번 실행 보증 방법' → 멱등성 ✅
- 멱등성은 최대 한번 실행을 보장하기 위한 핵심 개념이다.
- "멱등성은 수학 또는 컴퓨터 과학적 연산이 가질 수 있는 한 가지 속성으로, 연산을 여러번 실행해도 최초 실행 결과가 그대로 보존되는 특성을 일컫는다" By 위키백과
- API 관점에서 보자면 멱등성은 클라이언트가 같은 API 호출을 여러번 반복해도 항상 동일한 결과가 나온다는 뜻이다.
- 클라이언트(웹 및 모바일 애플리케이션)와 서버 간 통신을 위해선 일반적으로 클라이언트가 생성하고 일정 시간 지나면 만료되는 고유한 값을 멱등키로 사용한다.
- 스트라이프,페이팔 같은 많은 기술 회사가 UUID를 멱등키로 권장하며 실제로 널리 쓰인다.
- 결제 요청의 멱등성을 보장하기 위해선 HTTP 헤더에 <멱등키: 값> 형태로 멱등키를 추가한다.
시나리오1: 고객이 '결제'버튼 빠르게 더블 클릭하는 경우

- 사용자가 '결제'를 클릭하면 멱등키가 HTTP 요청 일부로 결제 시스템에 전송된다.
- 전자상거래 웹사이트에서 멱등키는 일반적으로 결제가 이루어지기 직전의 장바구니 ID다.
- 결제 시스템은 두번째 요청을 재시도로 처리하는데, 요청에 포함된 멱등키를 이전에 받은 적이 있기 때문이다. 그런 경우 결제 시스템은 이전 결제 요청의 가장 최근 상태를 반환한다.
- 동일한 멱등키로 동시에 많은 요청을 받으면 결제 서비스는 그 가운데 하나만 처리하고 나머지에 대해선
429 Too Many Requests 상태코드를 반환한다.
- 멱등성을 지원하는 한가지 방법은 데이터베이스의 고유키 제약조건을 활용하는 것이다! 예를 들어, 데이터베이스 테이블의 기본키를 멱등키로 사용한다.
- 결제 시스템은 결제 요청을 받으면 데이터베이스 테이블에 새 레코드를 넣으려고 시도한다.
- 새 레코드 추가에 성공했다는 것은 이전에 처리한 적 없는 결제 요청이라는 뜻이다.
- 새 레코드 추가에 실패했다는 것은 이전에 받은 적 있는 결제 요청이라는 뜻이다. 그런 중복 요청은 처리하지 않는다.
시나리오2: PSP가 결제를 성공적으로 처리했지만, 네트워크 오류로 응답이 결제 시스템에 전달되지 못해서 사용자가 '결제' 버튼을 다시 클릭하는 경우
- 결제 서비스는 PSP에 비중복 난수를 전송하고, PSP는 해당 난수에 대응하는 토큰을 반환한다. 이 난수는 결제 주문을 유일하게 식별하는 구실을 하며, 해당 토큰은 그 난수에 일대일로 대응한다. 따라서 토큰 또한 결제 주문을 유일하게 식별 가능하다.
- 사용자가 '결제' 버튼을 다시 누른다 해도 결제 주문이 같으니 PSP로 전송되는 토큰도 같다. PSP는 이 토큰을 멱등키로 사용하므로, 이중 결제로 판단하고 종전 실행 결과를 반환한다.
일관성
- 결제 실행 과정에서 상태 정보를 유지 관리하는 여러 서비스가 호출된다.
- 결제 서비스는 비중복 난수, 토큰, 결제 주문, 실행 상태 등 결제 관련 데이터를 유지 관리한다.
- 원장은 모든 회계 데이터를 보관한다.
- 지갑은 판매자의 계정 잔액을 유지한다.
- PSP는 결제 실행 상태를 유지한다.
- 데이터는 안정성을 높이기 위해 여러 데이터베이스 사본에 복제될 수 있다.
- 분산 환경에서는 서비스 간 통신 실패로 데이터 불일치가 발생할 수 있다.
- 지금부터 결제 시스템에서 발생 가능한 데이터 일관성 문제를 해결하는 기법을 살펴보자.
분산 환경에서 서비스 간 통신 실패로 인한 데이터 불일치 문제 해결 방법 ✅
- 내부 서비스 간 데이터 일관성을 유지하려면 요청이 '정확히 한번 처리'되도록 보장하는 것이 아주 중요하다.
- 내부 서비스와 외부 서비스(PSP)간 데이터 일관성 유지를 위해선 일반적으로 멱등성과 조정 프로세스를 활용한다.
- 외부 서비스가 멱등성을 지원하는 경우, 결제를 재시도할 땐 같은 멱등키를 사용해야 한다.
- 그러나 외부 서비스가 멱등 API를 지원하더라도, 외부 시스템이 항상 옳다고 가정할 순 없으므로, 조정 절차를 생략할 순 없다.
- 데이터를 다중화하는 경우엔 복제 지연으로 인해 기본 데이터베이스와 - 사본 데이터가 불일치하는 일이 생길 수 있다. 이 문제의 해결법은 다음과 같다.
- 주 데이터베이스에서만 읽기/쓰기 연산을 처리한다.
설정은 쉬우나, 규모 확장성이 떨어진다는 단점이 있다. 사본은 데이터 안정성 보장에만 활요되고 트래픽은 처리하지 않는다. 따라서 자원이 낭비된다.
- 모든 사본이 항상 동기화되도록 한다. 팩서스/래프트 같은 합의 알고리즘을 사용하거나, 합의 기반 분산 데이터베이스 사용한다.
보안
- 결제 보안은 매우 중요하다.
- 사이버 공겨과 카드 도난에 대응하기 위한 몇가지 기술을 간략히 살펴보자.
- 요청/응답 도청 → HTTPS 사용
- 데이터 변조 → 암호화 및 무결성 강화 모니터링
- 중간자 공격 → 인증서 고정 + SSL 사용
- 데이터 손실 → 여러 지역에 걸쳐 데이터베이스 복제 및 스냅숏 생성
- 분산 서비스 거부 공격(DDoS) → 처리율 제한 및 방화벽
- 카드 도난 → 토큰화. 실제 카드 번호를 사용하는 대신 토큰을 저장하고 결제에 사용
- PCI 규정 준수 → PIC DSS는 브랜드 신용카드를 처리하는 조직을 위한 정보 보안 표준이다.
- 사기 → 주소 확인, 카드 확인번호(CVV), 사용자 행동 분석 등.
4단계: 마무리
- 이번 장에서는 대금 수신, 정산 흐름을 살펴봤다.
- 재시도, 멱등성, 일관성
- 결제 오류 처리와 보안에 대한 사항
- 결제 시스템은 아주 복잡하며, 아직 더 언급할 가치 있는 주제가 남아있다.
- 모니터링
- 경보
- 디버깅 도구
- 환율
- 지역
- 현금 결제
- 구글/애플 페이 연동
2025.0202
2025.0211