[설계] 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 - 11장. 결제 시스템 ✍️(exactly-once 실행 보장 방법; 재시도+멱등키)

hailey·2025년 2월 2일

시스템설계

목록 보기
2/8

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)
    • 지갑에는 판매자의 계정 잔액을 기록한다.
    • 특정 사용자가 결제한 총 금액을 기록할 수도 있다.

일반적인 결제 흐름

  1. 사용자가 '주문하기' 버튼 클릭하면 결제 이벤트가 생성되어 결제 서비스로 전송된다.
  2. 결제 서비스는 결제 이벤트를 데이터베이스에 저장한다.
  3. 때로는 단일 결제 이벤트에 여러 결제 주문이 포함될 수 있다. 한 번 결제로 여러 판매자의 제품을 처리하는 경우가 그 예다. 전자상거래 웹사이트에서 한 결제를 여러 결제 주문으로 분할하는 경우, 결제 서비스는 결제 주문마다 결제 실행자를 호출한다.
  4. 결제 실행자결제 주문을 데이터베이스에 저장한다.
  5. 결제 실행자가 외부 PSP를 호출하여 신용 카드 결제를 처리한다.
  6. 결제 실행자가 결제를 성공적으로 처리하고 나면, 결제 서비스는 지갑을 갱신해서 특정 판매자의 잔고를 기록한다.
  7. 지갑 서버는 갱신된 잔고 정보를 데이터베이스에 저장한다.
  8. 지갑 서비스가 판매자 잔고를 성공적으로 갱신하면, 결제 서비스는 원장을 호출한다.
  9. 원장 서비스는 새 원장 정보를 데이터베이스에 추가한다.

결제 서비스 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. 모니터링 및 데이터 탐사에 필요한 도구가 풍부하게 지원되는가?
      1. 데이터베이스 관리자(DBA) 채용 시장이 성숙했는가?
        숙련된 DBA를 쉽게 채용할 수 있는가? (아주 중요한 요소다)
  • 일반적으로 NoSQL 보단 ACID 트랜잭션을 지원하는 전통적 관계형 데이터베이스를 선호한다.
  • 결제 이벤트 테이블에는 자세한 결제 이벤트 정보가 저장된다.

테이블 스키마

결제 이벤트

결제 주문

  • checkout_id : 외래키. 한 번의 결제 행위는 하나의 결제 이벤트를 만들고, 하나의 결제 이벤트에는 여러개의 결제 주문이 포함될 수 있다.
  • 구매자의 신용 카드에서 금액을 공제하기 위해 타사 PSP를 호출하면 판매자 대신 전자상거래 웹사이트의 은행 계좌에 이체가 이루어지는데, 이 프로세스를 대금 수신(pay-in)이라고 부른다.
    • 제품이 배송되는 등 대금 정산 조건이 충족되면 해당 대금을 판매자에게 정산하는 절차를 시작한다.
    • 그 결과로 전자상거래 웹사이트 은행 계좌에서 판매자의 은행 계좌로 금액이 이체된다.
    • 따라서 사용자의 결제를 처리하는 중 판매자의 은행 계좌가 아닌 구매자의 카드 정보만 필요하다.
  • payment_order_status : 결제 주문 실행 상태를 유지하는 Enum 타입이다.
    NOT_STARTED, EXECUTING, SUCCESS, FAILED
    • 업데이트 로직
      1. payment_order_status 초기값은 NOT_STARTED
      2. 결제 서비스는 결제 실행자에게 주문을 전송하면 EXECUTING으로 바꾼다.
      3. 결제 서비스는 결제 처리자의 응답에 따라 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와 연동한다.
      1. 회사가 민감한 결제 정보를 안전하게 저장할 수 있다면 API를 통해 PSP와 연동하는 방법을 택할 수 있다.
      • 회사는 결제 웹페이지를 개발하고 민감한 결제 정보를 수집하며, PSP는 은행 연결, 다양한 카드 유형을 지원하는 역할을 한다.
      1. 복잡한 규정 및 보안 문제로 인해 민감한 결제 정보를 저장하지 않기로 결정한 경우, PSP는 카드 결제 세부 정보를 수집해서 PSP에 안전하게 저장할 수 있도록 외부 결제 페이지를 제공한다. (대부분의 기업이 택하는 접근법)

외부 결제 페이지 이용 흐름

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

  • 외부 결제 페이지가 잘 동작할 때 시스템들이 어떻게 상호 연동하는지 설명했는데, 실제로 위 아홉단계 각각이 네트워크 문제로 실패할 수 있다.
  • 장애가 발생하면 체계적으로 처리할 수 있는 방법이 있을까? 조정(reconciliation)이 바로 그 방법이다!

조정(coordination)

  • 시스템 구성 요소가 비동기적으로 통신하는 경우 메시지가 전달되거나 응답이 반환된다는 보장이 없다.
  • 이는 시스템 성능을 위해 비동기 통신을 자주 사용하는 결제 관련 사업에 일반적인 문제다.
  • PSP나 은행 같은 외부 시스템도 비동기 통신을 선호한다.
  • 그렇다면 어떻게 정확성을 보장할 수 있을까? → 정답은 "조정"이다.
  • 관련 서비스 간 상태를 주기적으로 비교해서 일치하는지 확인하는 것이다.
  • 일반적으로 결제 시스템의 마지막 방어선으로 받아들여 진다.
  • 매일 밤 PSP나 은행은 고객에게 정산 파일을 보낸다.
  • 정산 파일에는 은행 계좌의 잔액과 하루 동안 해당 계좌에서 발생한 모든 거래내역이 기재되어 있다.
  • 조정 시스템은 정산 파일의 세부정보를 읽어 원장 시스템과 비교한다.
  • 조정은 결제 시스템의 내부 일관성을 확인할 때도 사용된다.
    예를 들어, 원장과 지갑의 상태가 같은지 확인할 수 있다.
  • 조정 중 발견된 차이는 일반적으로 재무팀에 의뢰해서 수동으로 고친다.

발생 가능한 불일치 문제 및 해결 방안

  1. 어떤 유형의 문제인지 알고 있고, 문제 해결 절차를 자동화 할 수 있는 경우 :
    원인과 해결 방법을 알고 있으며, 자동화 프로그램 작성이 비용 효율적인 경우다. 엔지니어는 발생한 불일치 문제의 분류와 조정 작업을 모두 자동화 할 수 있다.
  2. 어떤 유형의 문제인지는 알지만 문제 해결 절차를 자동화 할 수 없는 경우 : 수동으로 수정
  3. 분류할 수 없는 유형의 문제인 경우 : 불일치가 어떻게 발생하였는지 알지 못하는 경우.

결제 지연 처리

  • 결제 요청은 많은 컴포넌트를 거치며, 내부 및 외부의 다양한 처리 주체와 연동한다. 대부분의 경우 결제 요청은 몇 초 만에 처리되지만, 완료되거나 거부되기까지 몇 시간 또는 며칠이 걸리는 경우도 있다.
  • 다음은 결제 요청이 평소보다 오래 걸리게 되는 몇가지 사례다.
    • PSP가 해당 결제 요청의 위험성이 높다고 보고 담당자 검토를 요구하는 경우
    • 신용카드사가 구매확인 용도로 카드 소유자의 추가 정보를 요청하는 #D 보안 인증 같은 추가 보호 장치를 요구하는 경우
  • 결제 서비스는 처리하는데 시간이 오래 걸리는 이런 요청도 처리할 수 있어야 한다.
  • 구매 페이지가 외부 PSP에 호스팅 되는 경우(일반적인 관행) PSP는 다음과 같이 처리한다.
    • PSP는 결제가 대기(pending) 상태임을 알리는 상태 정보를 클라이언트에 반환하고, 클라이언트는 이를 사용자에게 표시한다. 클라이언트는 또한 고객이 현재 결제 상태를 확인할 수 있는 페이지도 제공한다.
    • PSP는 우리 회사를 대신해 대기중인 결제의 진행 상황을 추적하고, 상태가 바뀌면 PSP에 등록된 웹훅을 통해 결제 서비스에 알린다.
  • 결제 요청이 최종적으로 완료되면 PSP는 방금 언급한 사전에 등록된 웹훅을 호출한다. 결제 서비스는 내부 시스템에 기록된 정보를 업데이트 하고 고객에게 배송을 완료한다.
  • 어떤 PSP는 웹훅을 통해 결제 서비스에 결제 상태 변경을 알리는 대신, 결제 서비스로 하여금 대기 중인 결제 요청의 상태를 주기적으로 확인(polling)하도록 하기도 한다.

내부 서비스 간 통신

동기식 통신

  • HTTP 같은 동기식 통신은 소규모 시스템에서는 잘 작동하지만, 규모가 커지면 단점이 분명해진다.
  • 동기식 통신에서 한 요청에 응답을 만드는 처리 주기는 관련된 서비스가 많을수록 길어진다.
  • 단점
    • 성능 저하 : 요청 처리에 관계된 서비스 가운데 하나에 발생한 성능 문제가 전체 시스템의 성능에 영향을 미친다.
    • 장애 격리 곤란: PSP 등 서비스에 장애가 발생하면 클라이언트는 더이상 응답을 받지 못한다.
    • 높은 결합도: 요청 발신자는 수신자를 알아야만 한다.
    • 낮은 확장성: 큐를 버퍼로 사용하지 않고선, 갑작스러운 트래픽 증가에 대응할 수 있도록 시스템 확장이 어렵다.

비동기 통신

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

  • 일반적으로 동기식 통신은 설계하기 쉽지만 서비스 자율성을 높이기엔 적합하지 않다. 의존성 그래프가 커지면 전반적 성능은 낮아진다.
  • 비동기 통신은 설계의 단순성, 데이터 일관성을 시스템 확장성 및 장애 감내 능력과 맞바꾼 결과다. 비즈니스 로직이 복잡하고 타사 서비스 의존성이 높은 대규모 결제 시스템에서는 비동기 통신이 더 나은 선택이다! ✅

결제 실패 처리

  • 모든 결제 시스템은 실패한 결제를 적절히 처리할 수 있어야 한다.
  • 안정성 및 결함 내성은 결제 시스템의 핵심적 요구사항이다.

처리 방법1. 결제 상태 추적

  • 결제 주기의 모든 단계에서 결제 상태를 정확히 유지하는 것은 매우 중요하다.
  • 실패가 일어날 때마다 결제 거래의 현재 상태를 파악하고, 재시도 또는 환불이 불가한지 여부를 결정한다.
  • 결제 상태는 데이터 추가만 가능한(append-only) 데이터베이스 테이블에 보관한다.

처리 방법2. 재시도 큐 및 실패 메시지 큐 ⭐️

  • 실패를 우하하게 처리하기 위해서는 재시도 큐실패 메시지 큐를 두는것이 바람직하다.
    • 재시도 큐 : 일시적 오류 같은 재시도 가능 오류는 재시도 큐에 보낸다.
    • 실패 메시지 큐 : 반복적으로 처리에 실패한 메시지는 결국에는 실패 메시지 큐로 보낸다. 이 큐는 문제가 있는 메시지를 디버깅하고, 격리해서 성공적으로 처리되지 않은 이유를 파악하기 위한 검사에 유용하다.
    1. 재시도 가능한지 확인
    • a. 재시도 가능 실패는 재시도 큐로 보낸다.
    • b. 잘못된 입력 같이 재시도 불가능한 실패는 오류 내역을 데이터베이스에 저장한다.
    1. 결제 시스템은 재시도 큐에 쌓인 이벤트를 읽어서 실패한 결제를 재시도한다.
    1. 결제 거래가 다시 실패하는 경우엔 다음과 같이 처리한다.
    • a. 재시도 횟수가 임계값 이내라면, 해당 이벤트를 다시 재시도 큐로 보낸다.
    • b. 재시도 횟수가 임계값을 넘으면 해당 이벤트를 실패 메시지 큐에 넣는다. 이런 이벤트에 대해선 별도 조사가 필요할 수 있다.
  • 실무에서 이런 큐가 어떻게 쓰이는지 궁금하다면?
    우버에서 카프카를 활용해 결제 시스템 안정성과 결함 내성 요건을 어떻게 충족하고 있는지를 참고해보자!

⭐️'정확히 한 번' 전달 = '최소 한번 실행' + '최대 한번 실행'

  • 결제 시스템에 발생 가능한 가장 심각한 문제 중 하나는 고객에게 이중으로 청구하는 것이다.
  • 결제 주문이 정확히 한번만 실행되도록 결제 시스템을 설계하는 것이 중요하다.
  • 언뜻 보기엔 메시지를 정확히 한번 전달하는 것은, 매우 어려운 문제처럼 느껴지지만, 문제를 두부분으로 나누면 훨씬 쉽게 해결할 수 있다.
  • 수학적으로 보자면, 다음의 요건이 충족되면 주어진 연산은 정확히 한번만 실행된다.
      1. 최소 한번은 실행된다.
      1. 최대 한번 실행된다.
  • 재시도를 통해 최소 한번 실행을 보증하는 방법과, 멱등성 검사를 통해 최대 한번 실행을 보증하는 방법을 알아보자!

'최소 한번 실행 보증 방법' → 재시도 ✅

  • 네트워크 오류나 시간 초과로 인해 결제 거래를 다시 시도해야 하는 경우가 있다.
  • 재시도 메커니즘을 활용하면 어떤 결제가 최소 한번은 실행되도록 보장 가능하다.
  • 재시도 메커니즘을 도입할 땐, 얼마나 간격을 두고 재시도할지 정하는 것이 중요하다.
  • 재시도 전략
    • 즉시 재시도 : 클라이언트는 즉시 요청을 다시 보낸다.
    • 고정 간격 : 재시도 전에 일정 시간 기다린다.
    • 증분 간격 : 재시도 전에 기다리는 시간을 특정 양만큼 전진적으로 늘려 나간다.
    • 지수적 백오프 : 재시도 전에 기다리는 시간을 직전 재시도 대비 두배씩 늘려 나가는 방안이다.
    • 취소 : 요청을 철회하는 방안. 실패가 영구적이거나 재시도를 하더라도 성공 가능성이 낮은 경우에 흔히 사용되는 방안이다.
  • 적절한 재시도 전략을 결정하는 것은 어렵다. 다만 일반적으로 적용 가능한 지침은, 네트워크 문제가 단시간 내 해결될 것 같지 않다면 지수적 백오프를 사용하라는 것이다.
  • 지나치게 공격적인 재시도 전략은 컴퓨팅 자원을 낭비하고 서비스 과부하를 유발한다. 에러코드를 반환할 때는 Retry-After 헤더를 같이 붙여 보내는 것이 바람직하다.
  • 재시도 시 발생할 수 있는 잠제적 문제는 이중결제다.
    • 시나리오1: 결제 시스템이 외부 결제 페이지를 통해 PSP와 연동하는 환경에서 클라이언트가 결제 버튼을 두번 중복 클릭한다.
    • 시나리오2: PSP가 결제를 성공적으로 하였으나, 네트워크 오류로 인해 응답이 결제 시스템에 도달하지 못했다!!! 사용자가 '결제'버튼을 다시 클릭하거나 클라이언트가 결제를 다시 시도한다.
  • 이중 결제를 방지하려면 결제는 '최대 한번' 이루어져야 한다.
    '최대 한번 실행'은 다른말로 == 멱등성 이라고 한다.

'최대 한번 실행 보증 방법' → 멱등성 ✅

  • 멱등성은 최대 한번 실행을 보장하기 위한 핵심 개념이다.
  • "멱등성은 수학 또는 컴퓨터 과학적 연산이 가질 수 있는 한 가지 속성으로, 연산을 여러번 실행해도 최초 실행 결과가 그대로 보존되는 특성을 일컫는다" By 위키백과
  • API 관점에서 보자면 멱등성은 클라이언트가 같은 API 호출을 여러번 반복해도 항상 동일한 결과가 나온다는 뜻이다.
  • 클라이언트(웹 및 모바일 애플리케이션)와 서버 간 통신을 위해선 일반적으로 클라이언트가 생성하고 일정 시간 지나면 만료되는 고유한 값을 멱등키로 사용한다.
  • 스트라이프,페이팔 같은 많은 기술 회사가 UUID를 멱등키로 권장하며 실제로 널리 쓰인다.
  • 결제 요청의 멱등성을 보장하기 위해선 HTTP 헤더에 <멱등키: 값> 형태로 멱등키를 추가한다.

시나리오1: 고객이 '결제'버튼 빠르게 더블 클릭하는 경우

  • 사용자가 '결제'를 클릭하면 멱등키가 HTTP 요청 일부로 결제 시스템에 전송된다.
  • 전자상거래 웹사이트에서 멱등키는 일반적으로 결제가 이루어지기 직전의 장바구니 ID다.
  • 결제 시스템은 두번째 요청을 재시도로 처리하는데, 요청에 포함된 멱등키를 이전에 받은 적이 있기 때문이다. 그런 경우 결제 시스템은 이전 결제 요청의 가장 최근 상태를 반환한다.
  • 동일한 멱등키로 동시에 많은 요청을 받으면 결제 서비스는 그 가운데 하나만 처리하고 나머지에 대해선 429 Too Many Requests 상태코드를 반환한다.
  • 멱등성을 지원하는 한가지 방법데이터베이스의 고유키 제약조건을 활용하는 것이다! 예를 들어, 데이터베이스 테이블의 기본키를 멱등키로 사용한다.
      1. 결제 시스템은 결제 요청을 받으면 데이터베이스 테이블에 새 레코드를 넣으려고 시도한다.
      1. 새 레코드 추가에 성공했다는 것은 이전에 처리한 적 없는 결제 요청이라는 뜻이다.
      1. 새 레코드 추가에 실패했다는 것은 이전에 받은 적 있는 결제 요청이라는 뜻이다. 그런 중복 요청은 처리하지 않는다.

시나리오2: PSP가 결제를 성공적으로 처리했지만, 네트워크 오류로 응답이 결제 시스템에 전달되지 못해서 사용자가 '결제' 버튼을 다시 클릭하는 경우

  • 결제 서비스는 PSP에 비중복 난수를 전송하고, PSP는 해당 난수에 대응하는 토큰을 반환한다. 이 난수는 결제 주문을 유일하게 식별하는 구실을 하며, 해당 토큰은 그 난수에 일대일로 대응한다. 따라서 토큰 또한 결제 주문을 유일하게 식별 가능하다.
  • 사용자가 '결제' 버튼을 다시 누른다 해도 결제 주문이 같으니 PSP로 전송되는 토큰도 같다. PSP는 이 토큰을 멱등키로 사용하므로, 이중 결제로 판단하고 종전 실행 결과를 반환한다.

일관성

  • 결제 실행 과정에서 상태 정보를 유지 관리하는 여러 서비스가 호출된다.
  1. 결제 서비스는 비중복 난수, 토큰, 결제 주문, 실행 상태 등 결제 관련 데이터를 유지 관리한다.
  2. 원장은 모든 회계 데이터를 보관한다.
  3. 지갑은 판매자의 계정 잔액을 유지한다.
  4. PSP는 결제 실행 상태를 유지한다.
  5. 데이터는 안정성을 높이기 위해 여러 데이터베이스 사본에 복제될 수 있다.
  • 분산 환경에서는 서비스 간 통신 실패로 데이터 불일치가 발생할 수 있다.
  • 지금부터 결제 시스템에서 발생 가능한 데이터 일관성 문제를 해결하는 기법을 살펴보자.

분산 환경에서 서비스 간 통신 실패로 인한 데이터 불일치 문제 해결 방법 ✅

  • 내부 서비스 간 데이터 일관성을 유지하려면 요청이 '정확히 한번 처리'되도록 보장하는 것이 아주 중요하다.
  • 내부 서비스와 외부 서비스(PSP)간 데이터 일관성 유지를 위해선 일반적으로 멱등성조정 프로세스를 활용한다.
  • 외부 서비스가 멱등성을 지원하는 경우, 결제를 재시도할 땐 같은 멱등키를 사용해야 한다.
  • 그러나 외부 서비스가 멱등 API를 지원하더라도, 외부 시스템이 항상 옳다고 가정할 순 없으므로, 조정 절차를 생략할 순 없다.
  • 데이터를 다중화하는 경우엔 복제 지연으로 인해 기본 데이터베이스와 - 사본 데이터가 불일치하는 일이 생길 수 있다. 이 문제의 해결법은 다음과 같다.
      1. 주 데이터베이스에서만 읽기/쓰기 연산을 처리한다.
        설정은 쉬우나, 규모 확장성이 떨어진다는 단점이 있다. 사본은 데이터 안정성 보장에만 활요되고 트래픽은 처리하지 않는다. 따라서 자원이 낭비된다.
      1. 모든 사본이 항상 동기화되도록 한다. 팩서스/래프트 같은 합의 알고리즘을 사용하거나, 합의 기반 분산 데이터베이스 사용한다.

보안

  • 결제 보안은 매우 중요하다.
  • 사이버 공겨과 카드 도난에 대응하기 위한 몇가지 기술을 간략히 살펴보자.
  • 요청/응답 도청 → HTTPS 사용
  • 데이터 변조 → 암호화 및 무결성 강화 모니터링
  • 중간자 공격 → 인증서 고정 + SSL 사용
  • 데이터 손실 → 여러 지역에 걸쳐 데이터베이스 복제 및 스냅숏 생성
  • 분산 서비스 거부 공격(DDoS) → 처리율 제한 및 방화벽
  • 카드 도난 → 토큰화. 실제 카드 번호를 사용하는 대신 토큰을 저장하고 결제에 사용
  • PCI 규정 준수 → PIC DSS는 브랜드 신용카드를 처리하는 조직을 위한 정보 보안 표준이다.
  • 사기 → 주소 확인, 카드 확인번호(CVV), 사용자 행동 분석 등.

4단계: 마무리

  • 이번 장에서는 대금 수신, 정산 흐름을 살펴봤다.
  • 재시도, 멱등성, 일관성
  • 결제 오류 처리와 보안에 대한 사항
  • 결제 시스템은 아주 복잡하며, 아직 더 언급할 가치 있는 주제가 남아있다.
    • 모니터링
    • 경보
    • 디버깅 도구
    • 환율
    • 지역
    • 현금 결제
    • 구글/애플 페이 연동

2025.0202
2025.0211

profile
Fail Fast, Fail Often

0개의 댓글