결제 시스템
버튼 한 번으로 끝나 보이는 결제 뒤에는 고객·가맹점·PG·카드사가 주고받는 여러 단계가 있다. 카드 결제의 승인·매입·정산, PG 연동과 금액 검증, 간편결제와 토큰화, 이중 결제를 막는 멱등성과 상태 기계, 대사와 환불·차지백, 카드 데이터 보안 규정과 환율·정산 주기까지를 개발자 쪽에서 정리한다.
목차 (16)
이 노트는 결제가 어떻게 흘러가는지와 그 흐름을 코드로 받을 때 무엇을 지켜야 하는지를 다룬다. 수수료율·환율·정산 주기처럼 계약마다 다른 값은 원리를 보이려는 가상의 예이고, 실제 값은 쓰는 PG·카드사 계약서와 문서를 따른다. 법령은 2026년 10월에 국가법령정보센터에서 연 조문 기준이다. 결제 원장과 정확성이 중요한 시스템의 설계는 시스템 설계 › 정확성이 중요한 시스템, 멱등한 API 설계는 API 설계 노트에 있다.
결제에 등장하는 주체
카드 한 번 긁는 데에도 여러 회사가 끼어 있다. 고객(카드 회원)은 카드를 내밀고, 가맹점은 카드사와 계약을 맺고 카드를 받는 판매자다. 온라인에서는 가맹점이 카드사마다 따로 계약하는 대신 PG(결제대행)에 맡긴다. 여신전문금융업법은 이런 곳을 "신용카드등에 의한 거래를 대행하는 자", 곧 결제대행업체로 정의하고 가맹점의 하나로 본다(제2조 제5호 나목). 오프라인 단말기와 카드사 사이에서 승인·매입 전문을 실어 나르는 곳은 VAN(부가통신업자)이다. 법은 이를 단말기 설치, 조회·승인, 매출전표 매입·자금정산을 위한 전기통신서비스로 정의한다(제2조 제8호의2).
카드 쪽에는 카드를 내주고 고객에게 청구하는 발급사(issuer), 가맹점의 매출을 사들여 대금을 주는 매입사(acquirer), 그리고 Visa·Mastercard처럼 서로 다른 나라의 발급사와 매입사를 잇는 카드 네트워크가 있다. 국내 카드 거래에서는 카드사 한 곳이 이 일을 함께 하는 경우가 많다. 법이 정한 신용카드업 자체가 카드 발행·관리, 대금 결제, 가맹점 모집·관리를 아우른다(제2조 제2호).
카드 결제 흐름: 승인 → 매입 → 정산
카드 결제는 세 단계로 나뉜다. 승인(authorization)은 카드사에 "이 카드로 이 금액을 쓸 수 있는가"를 묻고, 된다면 그만큼 한도를 잡아 두는 일이다. 이 순간 돈은 움직이지 않는다. 매입(capture, 국내에서는 매출 전표 매입)은 가맹점이 승인된 거래를 카드사에 청구해 "이 매출을 사 달라"고 하는 일이고, 매입이 확정돼야 카드사가 대금을 줄 의무가 생긴다. 정산(settlement)은 카드사가 수수료를 뗀 돈을 가맹점(또는 PG) 계좌로 보내는 일이다. 예를 들어 50,000원 결제에 가상의 수수료율 2.00%면 가맹점은 49,000원을 받는다.
고객이 카드로 50,000원을 결제한다. 요청은 가맹점과 PG(또는 VAN)를 거쳐 카드사에 닿고, 카드사는 카드가 유효한지, 한도가 남았는지, 이상 거래인지 본다.
단계가 나뉘어 있어서 시점도 어긋난다. 고객 카드 앱에는 승인 직후 "승인" 내역이 뜨지만 청구는 매입 뒤에 잡히고, 가맹점 통장에 돈이 들어오는 날은 정산 주기에 따라 며칠 뒤다. 결제 시스템이 "결제 완료" 하나로 끝나지 않고 상태를 여럿 갖는 이유다.
승인과 매입의 차이
대부분의 온라인 결제는 승인과 매입이 거의 동시에 일어나지만, 둘을 일부러 떼어 놓는 경우가 있다. 호텔은 체크인 때 숙박비를 먼저 승인해 두고(가승인, 홀드) 퇴실 때 실제 금액만 매입한다. 120,000원을 승인하고 95,000원만 매입하면 남은 25,000원은 해제된다. 렌터카·주유·주문 제작처럼 최종 금액을 나중에 아는 곳도 같은 방식을 쓴다.
- 승인에는 기한이 있다. Stripe 문서 기준으로 온라인 카드 승인은 대개 7일 동안 유지되고, 카드 브랜드와 고객이 직접 결제했는지(CIT)·가맹점이 걸었는지(MIT)에 따라 더 짧기도 하다. 기한을 넘기면 홀드가 풀리고 결제는 취소 상태가 된다.
- 취소와 환불은 다르다. 매입 전이면 승인을 취소하면 끝이라 매출이 생기지 않는다. 매입 뒤라면 이미 생긴 매출을 되돌리는 환불이 되고, 처리 수수료가 돌아오지 않을 수 있다.
- 부분 취소는 남은 승인을 푸는 일이다. 일부만 매입하면 차액은 자동으로 해제된다. 대부분 한 승인에 매입은 한 번이라, 나머지를 나중에 다시 매입하려면 새 결제를 만들어야 한다.
수수료 구조
카드 결제의 비용은 대부분 가맹점이 내는 가맹점 수수료다. 결제 금액의 일정 비율을 정산 때 떼고, 그 안에서 카드사(신용을 빌려주고 부정 사용 위험과 포인트 비용을 지는 대가), 해외 카드라면 카드 네트워크, 그리고 중계를 맡은 VAN이나 결제창·정산을 대신하는 PG가 몫을 나눈다. PG를 쓰는 온라인 가맹점은 보통 PG와 맺은 하나의 수수료율만 보지만, 그 안에 이런 몫이 들어 있다.
한국에서는 법이 수수료를 직접 다룬다. 여신전문금융업법 제18조의3은 카드사가 가맹점 수수료율을 공정하고 합리적으로 정하고 부당하게 차별하지 말 것, 그리고 대통령령으로 정한 규모 이하의 영세한 중소 가맹점에는 금융위원회가 정하는 우대수수료율을 적용할 것을 정한다. 거꾸로 큰 가맹점이 우월적 지위로 부당하게 낮은 수수료율이나 보상금을 요구하는 것도 금지한다. 제19조는 가맹점이 수수료를 카드 회원에게 떠넘기는 것을 금지한다. 구체적인 우대 구간과 비율은 시행령과 금융위원회 고시로 정해지고 바뀌므로, 이 노트에 숫자를 적지 않는다.
PG 연동 흐름과 금액 검증
온라인 결제를 붙일 때 개발자가 직접 다루는 것은 PG와의 연동이다. 토스페이먼츠의 해설처럼 많은 PG가 결제를 요청 → 인증 → 승인으로 나눈다. 결제창에서 고객이 카드를 고르고 인증까지 마치면, PG는 브라우저를 가맹점의 성공 URL로 돌려보내며 결제 키·주문 번호·금액을 넘긴다. 그다음 가맹점 서버가 승인 API를 불러야 결제가 끝난다. 인증과 승인을 나누면 가맹점 서버가 결과를 직접 받아 승인하므로 "PG는 완료, 가맹점은 실패" 같은 어긋남이 줄어든다.
결제창을 열기 전에 서버가 주문 번호와 최종 금액(쿠폰·적립금을 반영한 50,000원)을 먼저 저장한다. 나중에 맞춰 볼 기준값이다.
- 브라우저가 준 값은 믿지 않는다. 금액은 결제창을 열기 전에 서버에 저장한 주문 금액과 비교한다. 다르면 승인을 부르지 않는다. 상품 가격·할인을 클라이언트가 계산해 보내게 하면 안 되는 것과 같은 원리다.
- 비밀 키는 서버에만 둔다. 승인 API를 부르는 키가 프런트엔드 코드나 앱에 들어가면 누구나 승인·취소를 부를 수 있다. 환경 변수나 비밀 관리 도구에 두고, 결제 화면에는 공개용 키만 쓴다.
- 기한이 있다. 토스페이먼츠 문서 기준으로 결제창에서 30분 안에 인증하지 않거나 인증 뒤 10분 안에 승인 API를 부르지 않으면 결제가 만료된다. 승인 요청이 늦어지는 경로(큰 트랜잭션, 외부 호출)를 앞에 두지 않는다.
- 응답을 못 받으면 상태를 묻는다. 승인 요청이 타임아웃 나면 "실패"로 단정하지 않는다. 실제로는 승인됐을 수 있으니 같은 멱등성 키로 재시도하거나 조회 API로 확인한다(아래 정합성 절).
간편결제와 토큰화
간편결제는 카드나 계좌를 한 번 등록해 두고 비밀번호·생체 인증만으로 결제하는 방식이다. 그 바탕이 토큰화다. EMVCo의 결제 토큰은 카드 번호(PAN)를 고유한 대체 값으로 바꾸고, 그 토큰을 특정 가맹점·기기·결제 상황 밖에서는 쓸 수 없게 묶는다. 휴대폰 지갑에 넣은 카드도 기기 전용 토큰으로 결제한다. 토큰이 새어 나가도 다른 곳에서 결제에 쓸 수 없고, 토큰만 폐기하면 되니 카드를 다시 발급할 필요가 없다.
가맹점이 정기 결제나 원클릭 결제를 만들 때도 같은 원리를 쓴다. PG에 카드를 등록하면 PG가 빌링 키(카드 토큰)를 주고, 가맹점은 다음 결제부터 그 키로 결제를 요청한다. 가맹점 데이터베이스에는 카드 번호가 아니라 키만 남는다.
계좌이체·가상계좌·휴대폰 결제
카드 말고도 수단마다 돈의 길과 확정 시점이 다르다. 계좌이체는 고객 계좌에서 바로 출금된다. 가상계좌는 주문마다 입금 전용 계좌 번호를 내주고 고객이 직접 송금하는 방식이라, 주문 시점에는 결제가 끝나지 않고 "입금 대기"로 남는다. 입금 사실은 PG가 웹훅으로 알려 주므로 웹훅을 받지 않으면 주문을 확정할 수 없다. 토스페이먼츠 문서에는 송금 한도 초과 같은 입금 오류가 나면 상태가 완료에서 입금 대기로 되돌아가는 경우도 적혀 있다. 휴대폰 결제는 통신 요금에 합쳐 청구되고, 가맹점 정산은 통신사가 요금을 거둔(수납) 뒤에 이뤄지는 방식이 있다.
정합성: 이중 결제, 멱등성, 웹훅
결제 코드에서 가장 무서운 버그는 돈이 두 번 빠지거나, 빠졌는데 주문이 없는 것이다. 원인은 대개 네트워크다. 요청은 성공했는데 응답이 끊기면 클라이언트는 결과를 모른 채 다시 보내고, 서버는 새 요청으로 처리해 버린다. 이를 막는 것이 멱등성 키다. 클라이언트가 요청마다 충분히 무작위한 키(UUID 등)를 붙이면, 서버는 같은 키의 재시도를 알아보고 저장해 둔 첫 응답을 그대로 돌려준다. Stripe는 첫 요청의 상태 코드와 본문을 저장하고, 같은 키에 파라미터가 다르면 오류를 내며, 키는 최소 24시간 뒤에 지울 수 있다고 설명한다. 키에 이메일 같은 개인정보를 쓰지 말라는 주의도 있다.
주문 A-17을 30,000원 결제한다. 서버는 처리했지만 응답이 오는 길에 연결이 끊겨 클라이언트는 결과를 모른다.
다음은 결제 상태 기계다. 결제를 "성공/실패" 불리언으로 두지 않고 허용된 전이만 있는 상태로 둔다. 대기에서 승인으로, 승인에서 매입으로, 매입에서 정산으로 가는 것이 정상 경로이고, 취소·환불·실패는 더 나가지 않는 끝 상태다. 표에 없는 전이(이미 환불된 결제에 매입이 오는 등)는 버그나 늦게 도착한 메시지이므로 반영하지 않고 경보를 낸다. 상태를 바꿀 때는 "지금 상태가 승인일 때만 매입으로" 같은 조건부 갱신을 써서 동시에 온 두 요청이 둘 다 이기지 못하게 한다.
PG가 상태 변화를 알려 주는 웹훅은 "적어도 한 번" 배달이다. 받는 쪽이 제때 2xx로 답하지 않으면 다시 보낸다. 토스페이먼츠는 10초 안에 200 응답을 요구하고, 실패하면 1, 4, 16, 64, 256, 1024, 4096분 간격으로 최대 7번 다시 보낸다. Stripe는 운영 모드에서 최대 3일 동안 지수 백오프로 다시 보내고, 순서를 보장하지 않는다고 밝힌다. 그래서 웹훅 처리기는 처리한 사건 id를 기록해 중복을 버리고, 상태 기계로 순서가 뒤바뀐 사건을 걸러야 한다. 서명을 먼저 검증하지 않으면 누구나 가짜 "결제 완료"를 보낼 수 있다.
주문 데이터베이스와 PG처럼 서로 다른 시스템의 상태를 한 트랜잭션으로 묶을 수는 없다. 대신 결제 의도를 먼저 기록하고(아웃박스), 외부 호출 결과를 멱등하게 반영하며, 끝까지 어긋난 것은 대사로 찾는다. 분산 트랜잭션과 그 한계는 분산 시스템 노트, 이중 기입 원장과 결제 설계 사례는 시스템 설계 › 정확성이 중요한 시스템에 있다.
대사(reconciliation)
멱등성과 웹훅으로도 모든 어긋남을 막지는 못한다. 그래서 정기적으로 대사를 한다. 우리 장부(주문·결제 기록)와 PG가 주는 정산 내역을 결제 id로 맞춰 보고, 일치하지 않는 건을 사람이 보거나 자동으로 고친다. 아래 예에서 장부 5건과 정산 내역 5건을 맞추면 3건이 어긋난다. 금액이 다른 건(부분 취소가 장부에 빠졌을 수 있다), 장부에만 있는 건(정산 전이거나 승인이 실패했는데 주문만 남았다), PG에만 있는 건(결제는 됐는데 주문이 없는, 가장 급한 경우)이다.
대사는 세 겹으로 한다. 주문과 PG 거래 내역, PG 거래 내역과 정산 명세(수수료 포함), 정산 명세와 실제 은행 입금이다. 금액은 부동소수점이 아니라 정수(원, 센트 같은 최소 단위)로 다뤄야 합계가 1원도 틀리지 않는다.
환불과 부분 환불
환불은 매입된 결제를 되돌리는 새 거래다. 대부분의 PG는 한 결제에 여러 번 나눠 환불할 수 있게 하되, 누적 환불액이 원래 금액을 넘지 못하게 막는다. 환불은 원래 결제 수단으로만 돌아가고, 카드사를 거쳐 고객 명세서에 보이기까지 며칠이 걸린다(Stripe 기준 대략 5~10영업일). 결제 직후의 환불은 새 환불 대신 원거래 취소(reversal)로 처리돼 명세서에서 아예 사라지기도 한다.
- 환불도 실패할 수 있다. 카드가 해지됐거나 계좌가 닫혔으면 돈이 되돌아온다. 환불 상태를 웹훅으로 받아 실패하면 다른 방법을 안내한다.
- 가상계좌·계좌이체 환불은 돌려줄 계좌 정보를 고객에게 받아야 하는 경우가 많다. 그동안의 상태를 따로 둔다.
- 쿠폰·적립금이 섞인 주문은 부분 환불 때 어느 몫을 먼저 돌려줄지 규칙을 정해 둔다. 장부와 PG의 환불 금액이 어긋나는 흔한 원인이다.
차지백
차지백(chargeback, 분쟁)은 고객이 가맹점이 아니라 발급사에 결제에 이의를 제기하는 것이다. 물건을 받지 못했거나, 카드를 도용당했거나, 취소했는데 청구됐다는 이유다. Stripe 문서에 따르면 발급사가 카드 네트워크에 분쟁을 올리면 결제가 즉시 되돌려지고, 대금과 네트워크 분쟁 수수료가 매입 쪽을 거쳐 가맹점 잔액에서 빠진다. 가맹점은 배송 기록·이용 로그 같은 증빙을 내서 다투거나 받아들일 수 있고, 판정은 발급사와 네트워크 규칙이 한다.
개발 쪽에서 할 일은 증빙을 남기는 것이다. 주문·배송·로그인 기록을 결제 id와 이어 두고, 분쟁 웹훅을 받으면 해당 주문의 상태를 바꾸고 담당자에게 알린다. 고객이 쉽게 환불을 요청할 수 있으면 분쟁까지 가는 일이 줄고, 분쟁 비율이 높으면 네트워크의 감시 프로그램 대상이 될 수 있다.
보안과 규정
카드 데이터를 다루는 곳이 지키는 국제 기준이 PCI DSS다. 결제 계정 데이터를 보호하기 위한 기술·운영 요구사항의 기준선이고, 지금 유효한 판은 2024년 6월 11일에 나온 v4.0.1이다. v4.0은 2024년 12월 31일에 은퇴했고, v4.0에서 미래 시행으로 둔 요구사항은 2025년 3월 31일부터 적용됐다(PCI SSC 블로그). 가장 실무적인 원칙은 카드 번호를 직접 받지도, 저장하지도 않는 것이다. PG 결제창·SDK가 카드 정보를 받게 하면 카드 번호가 우리 서버를 지나지 않아 지켜야 할 범위가 크게 준다. 카드 뒷면의 보안 코드는 승인 뒤에는 암호화하든 고객이 동의하든 보관할 수 없다(PCI SSC FAQ).
한국 법령도 결제에 직접 닿는다. 여신전문금융업법 제19조는 가맹점이 카드 결제를 이유로 결제를 거절하거나 카드 회원을 불리하게 대우하는 것, 수수료를 회원에게 부담시키는 것, 실제 매출 없이 카드 거래를 꾸미는 것을 금지한다. 전자금융거래법은 전자지급결제대행(PG) 업무를 하려면 금융위원회에 등록하게 하고(제28조 제2항), 금융회사와 전자금융업자에게 전자금융거래의 안전성 확보 의무를 지운다(제21조). 고객의 이름·연락처·주소 같은 개인정보를 다루는 원칙은 개인정보 보호 기술 노트에 있다. 이 절은 법률 조언이 아니다. 실제 서비스는 계약한 PG와 전문가에게 확인한다.
해외 결제와 환율
해외 결제에는 통화가 셋 끼어든다. 고객 카드의 통화, 결제를 표시하고 청구하는 결제 통화(presentment), 가맹점 계좌가 받는 정산 통화(settlement)다. 결제 통화와 정산 통화가 다르면 PG가 바꿔서 정산하고, 결제 통화와 고객 카드 통화가 다르면 발급사가 고객에게 해외 결제 수수료를 물릴 수 있다. API는 금액을 통화의 최소 단위 정수로 받는다. 10.99달러는 1099이고, 원·엔처럼 소수가 없는 통화는 금액 그대로다.
정산 주기
정산 주기는 매출일에서 며칠 뒤에 돈이 들어오는지다. 흔히 D+n으로 쓰고 n은 영업일로 센다. 토스페이먼츠의 안내를 보면 일정산·주정산·월정산처럼 주기가 여럿이고, 지급은 영업일에만 하며, 입금일 2영업일 전에 지급 금액을 확정한다. 금요일 매출을 D+2로 정산하면 주말을 건너 11월 3일 화요일에 들어온다. 현금 흐름 계획과 대사 모두 이 날짜를 기준으로 하므로, 영업일·공휴일 계산을 한 곳에 모아 테스트해 둔다.
흔한 오해
- "승인되면 돈이 들어온 것이다" — 승인은 한도를 잡아 둔 것일 뿐이다. 매입이 돼야 매출이 되고, 정산이 돼야 통장에 들어온다. 승인만 하고 매입하지 않으면 기한이 지나 풀린다.
- "결제창이 성공 URL로 돌아왔으니 끝났다" — 리다이렉트는 인증 결과일 뿐이다. 서버가 금액을 검증하고 승인 API를 불러 성공 응답을 받아야 결제가 끝난다.
- "타임아웃이면 실패다" — 응답만 못 받았을 수 있다. 같은 멱등성 키로 다시 묻거나 조회 API로 확인하기 전에는 실패로 처리하지 않는다.
- "웹훅은 한 번, 순서대로 온다" — 중복도 오고 순서도 바뀐다. 사건 id로 중복을 거르고 상태 기계로 전이를 검사한다.
- "금액은 float로 충분하다" — 0.1 + 0.2는 정확히 0.3이 아니다. 최소 단위 정수나 십진 고정소수점 타입을 쓴다.
- "카드 번호를 암호화해 두면 괜찮다" — 암호화한 카드 번호도 PCI DSS의 보호 대상이고 키 관리 부담이 따른다. 보안 코드는 아예 보관할 수 없다. 처음부터 토큰만 갖는 편이 낫다.
핵심 자료
요지는 직접 정리한 한 줄이다. 결제를 포함한 정확성 중심 설계 사례는 시스템 설계 노트, 멱등성 키를 받는 API 설계는 API 설계 노트에 있다.
| 자료 | 종류 | 요지 | 링크 |
|---|---|---|---|
| 여신전문금융업법 | 법령 | 신용카드업·가맹점·결제대행업체·부가통신업(VAN)의 정의(제2조), 수수료율 차별 금지와 영세·중소 가맹점 우대수수료율(제18조의3), 가맹점 준수사항(제19조). | 국가법령정보센터 |
| 전자금융거래법 | 법령 | 전자금융거래·전자지급거래의 정의(제2조), 안전성 확보 의무(제21조), 전자지급결제대행(PG) 등 전자금융업의 등록(제28조). | 국가법령정보센터 |
| PCI DSS | 보안 표준 | 카드 계정 데이터를 다루는 곳이 지켜야 할 기술·운영 요구사항의 기준선. 현재 v4.0.1이다. | PCI SSC |
| Can card verification codes be stored for card-on-file or recurring transactions? | FAQ | 보안 코드는 승인 뒤 보관할 수 없다. 암호화해도, 고객이 동의해도 마찬가지다. | PCI SSC |
| EMV Payment Tokenisation | 기술 표준 | 카드 번호를 특정 가맹점·기기·결제 상황에서만 쓸 수 있는 토큰으로 바꾸는 방식. | EMVCo |
| 결제 요청, 인증, 승인… 이게 다 뭔가요? | PG 해설 | 요청·인증·승인을 나누는 이유와, 승인 전에 저장해 둔 금액으로 검증하는 흐름. | 토스페이먼츠 |
| 웹훅 연결하기 | PG 문서 | 10초 안에 200 응답, 실패하면 1·4·16·64·256·1024·4096분 간격으로 최대 7번 재전송. | 토스페이먼츠 |
| Idempotent requests | API 문서 | 멱등성 키로 재시도를 안전하게 만든다. 첫 응답을 저장하고, 같은 키에 다른 파라미터가 오면 오류를 낸다. | Stripe |
| Receive Stripe events in your webhook endpoint | API 문서 | 서명 검증, 빠른 2xx 응답, 중복·순서 뒤바뀜 대비, 최대 3일 재전송. | Stripe |
| Place a hold on a payment method | API 문서 | 승인과 매입을 나누는 수동 매입, 승인 유효 기간, 부분 매입 시 남은 금액 해제. | Stripe |
| Disputes | API 문서 | 차지백이 시작되면 대금과 분쟁 수수료가 먼저 빠지고, 가맹점은 증빙으로 다툰다. | Stripe |
- 여신전문금융업법 — 국가법령정보센터
- 전자금융거래법 — 국가법령정보센터
- PCI DSS — PCI SSC
- Can card verification codes be stored for card-on-file or recurring transactions? — PCI SSC
- EMV Payment Tokenisation — EMVCo
- 결제 요청, 인증, 승인… 이게 다 뭔가요? — 토스페이먼츠
- 웹훅 연결하기 — 토스페이먼츠
- Idempotent requests — Stripe
- Receive Stripe events in your webhook endpoint — Stripe
- Place a hold on a payment method — Stripe
- Disputes — Stripe
- Just Published: PCI DSS v4.0.1 — PCI SSC 블로그
- 웹훅 이벤트(결제 상태 변화) — 토스페이먼츠
- 정산 계좌·주기·한도를 바꿨다면, 입금 일자는 언제 변경될까? — 토스페이먼츠
- Refund and cancel payments — Stripe
- Supported currencies(결제 통화와 정산 통화) — Stripe