들어가며
저는 예전부터 '결제'에 대한 궁금증이 있었습니다. 돈을 다루는 만큼 정합성을 아주 중요히 봐야 하지만, 성능도 함께 챙겨야 하기 때문에 엔지니어가 여러 고민을 하고 다양한 시도를 해볼 수 있는 도메인이라고 생각했었거든요. 단순한 결제 연동을 넘어, 결제라는 도메인에서 발생하는 복잡한 데이터 흐름과 어려움들을 직접 경험해보고 다뤄보고 싶다는 마음이 생겼었습니다.
우선 결제를 처리할 때 PG사 내부에서 어떤 일들이 벌어지고 있는지 또 어떤 고민들이 일어나고 있는지 알아보고 싶었습니다. 그래서 요즘은 퇴근 후 토이 성격의 PG를 만들면서 가맹점 서버가 아닌 PG사 입장에서 결제 준비 ~ 카드사 승인 등의 흐름을 직접 공부하고 설계하고 있는데요, 그 첫 번째 기록으로 이 글을 쓰고 있습니다.
글 시작 전 2가지를 명확히 하자면 이 프로젝트는 카드결제를 기준으로 하고, 또 토이 프로젝트라는 점입니다. 토스페이먼츠, 나이스페이먼츠 등 여러 PG 사의 docs와 테스트 환경을 실제로 분석해보면서 PG사 관점의 결제 흐름을 최대한 모사하려 했지만, PG사와 카드사, VAN 등의 내부 통신을 정확히 알 수는 없었기 때문에 결국 "이렇게 되고 있을 것이다"라고 가정하고 처리하는 부분들이 있습니다. 따라서 이 프로젝트에서 사용한 결제 흐름이 실제와는 차이가 있을 수 있습니다만, 최대한 분석하며 실제에 가깝게 만들고자 했습니다.
목차
1. 결제 도메인 기본 이해
2. '인증결제'의 흐름 살펴보기
3. mini-pg 설계
1. 결제 도메인 기본 이해
결제 주요 참여자
각 PG사들의 docs들을 여러 번 읽으며 결제 흐름과 관련 용어들을 공부해봤는데요. 결제에 관여하는 주요 요소들은 다음처럼 정리해볼 수 있습니다.

1. 고객
카드사로부터 카드를 발급받은 고객을 말합니다
2. 가맹점
물건을 파는 사업자로, 결제 인프라를 직접 만들지 않고 PG사에 가입하게 됩니다.
3. PG사
Payment Gateway의 줄임말로, 카드사들을 한 데 모아 가맹점에 제공하는 회사의 역할을 합니다. 고객들이 보는 결제창, 결제 API, 정산 등을 책임집니다. 이 토이 프로젝트(mini-pg)에 해당하는 요소입니다!
4. VAN
Value Added Network의 줄임말로, PG사와 카드사 사이, 또는 오프라인 단말기와 카드사 사이에서 거래 데이터를 중계하는 통신망 중계 사업자입니다.
5. 카드사
두 종류가 있습니다.
- 발급사 : 실물 카드를 발급하고 회원을 관리하는 곳입니다.
- 매입사 : 카드 거래를 처리하고 가맹점에 대금을 입금해 주는 곳입니다. 참고로 국내엔 총 9개(국민, 삼성, 신한, 현대, 하나, 비씨, 롯데, 농협, 우리)가 있습니다.
6. 은행 + 금융결제원
실제 원화 자금이 오가는 인프라로, 카드사가 가맹점에 대금을 지급할 때 또는 PG사가 가맹점에 정산금을 보낼 때 등이 모두 은행 계좌이체로 이뤄지고 그 중계가 금융결제원 망을 통해 수행됩니다.
mini-pg 에서는 가맹점과 PG, 카드사만 모델링했습니다.
결제 기본 흐름
결제의 본질은 결국 고객과 가맹점 사이에서 직접 돈이 오가는게 아니라, 믿을 수 있는 제 3자(PG사, 은행, 카드사)가 고객의 돈을 가맹점으로 대신 전달해주는 것으로 볼 수 있습니다. 이 관점에서 결제는 크게 다음 4가지 순서로 진행된다고 이해할 수 있습니다.

1. 인증
“지금 이 사람이 이 카드의 주인이 진짜 맞는지”를 보는 단계입니다.
2. 승인
카드가 유효한지 확인하고 돈을 잡아두는 단계로 신용카드 결제는 이 단계에서 실제 돈이 나가지 않고 홀드만 하는 반면(한도만 차감한다고 표현), 체크카드는 이 단계에서 돈이 나가게 됩니다.
3. 매입
PG사가 승인된 거래들을 모아서 카드사로 대금 지급을 청구하는 단계로, 카드사는 매입이 완료되면 PG사로 돈을 줍니다.
4. 정산
매입이 완료되어 PG사에 온 돈을 수수료를 떼고 가맹점으로 주는 단계를 말합니다.
mini-pg 에서는 승인에 초점을 맞춰 다뤄보기로 했습니다
여기서 결제의 흐름 쪽에서 어떤 데이터들이 오고 가는지를 좀 더 중점적으로 살펴봤습니다.
2. '인증결제'의 흐름 살펴보기
아래 내용은 분석 과정에서 제가 가정한 결제 흐름들을 포함합니다. PG의 실제 결제 흐름과는 다를 수 있습니다.
결제 흐름 모사를 위해 토스페이먼츠, 나이스 페이먼츠 등 각 PG사에서 퍼블릭하게 제공하는 연동 가이드들을 정말 수차례 살펴봤습니다. 해당 가이드들에서는 인증결제(고객이 결제수단의 소유자라는 본인인증을 마치고 결제하는 것)를 위주로 다루고 있는데요. 결제 도메인이 처음인 제 입장에서 특히 혼란스러웠던 점은 아래처럼 가맹점이 PG의 결제승인 API를 호출할 때의 payload에는 카드번호가 없는데 응답에는 카드번호가 있다는 점이었습니다.
1) 토스페이먼츠 결제승인 API 요청 & 응답 예제
curl --request POST \
--url https://api.tosspayments.com/v1/payments/confirm \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5Bc...' \
--header 'Content-Type: application/json' \
--data '{
"paymentKey":"5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
"orderId":"a4CWyWY5m89PNh7xJwhk1",
"amount":1000
}'
{
"mId": "tosspayments",
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
"orderId": "a4CWyWY5m89PNh7xJwhk1",
"card": {
"issuerCode": "71",
"acquirerCode": "71",
"number": "12345678****000*",
"cardType": "신용",
"ownerType": "개인",
, ...생략
},
, ...생략
}
2) 나이스페이먼츠 결제승인 API 요청 & 응답 예제
curl --request POST \
--url https://sandbox-api.nicepay.co.kr/v1/payments/UT0000113m01012111051714341073 \
--header 'Authorization: Basic UzJfYWY0NTQz...' \
--header 'Content-Type: application/json' \
--data '{
"amount": 1004
}'
{
"tid": "UT0000113m01012111051714341073",
"card": {
"cardCode": "04",
"cardName": "삼성",
"cardNum": "12341234****1234",
, ... 생략
},
, ... 생략
}
"PG의 결제 승인 API를 호출할 땐 카드번호를 넘기지 않는데, 어떻게 사용자가 선택한 카드로 승인이 일어나고 있으며 어떻게 응답 내용에는 마스킹된 카드번호가 담겨 있을까?" 라는 의문이 자연스럽게 생길 수밖에 없었습니다. PG사들이 제공하는 가이드 문서로는 결제 승인 API의 형식 정도만 말해주고 있기 때문에, 인증 단계에서 무슨 일이 벌어지는지, 카드번호가 어떻게 전달되는지 등은 직접 분석하면서 가정할 수밖에 없었습니다.
물론 "어떻게 되고 있는 건지는 모르겠다. 어차피 토이인데 비인증 결제만 다룬다 치고, 우리는 카드번호를 payload에 담는 결제 승인 API만 만드는 걸로 하자!" 라고 할 수도 있었지만, 실제에 가까운 프로젝트를 모사 + 결제 도메인 공부도 해볼 겸 인증결제는 어떻게 진행되고 있는지 좀 더 뜯어봤습니다.
개인적으론 "PG는 사용자나 가맹점으로부터 결제에 쓰일 카드번호를 받고 있고, PG사가 카드사로 승인요청을 보낼때 그 카드번호를 활용한다" 라는 가정을 하고 있었던 터라 여러 시행착오를 겪었는데요. 결론적으로 어찌저찌 알아낸 점들은 다음과 같았습니다.
(PG사별로 테스트 환경을 제공해주는게 너무 고마웠습니다.)
1. 인증결제는 사용자 or 가맹점에서 PG로 카드번호를 보내지 않으며, 인증은 사용자와 카드사끼리 즉 PG 밖에서 발생 (관련있는 보안 표준으로 pci-dss가 있습니다)
2. 즉 'PG → 카드사로의 승인요청'은 카드번호를 기준으로 라우팅하는게 아니며, 실제 결제에 사용될 카드번호는 카드사가 안다
3. 결제창을 연 뒤로는 브라우저에서 PG로 요청을 보내면서 '이 결제의 인증완료 여부'를 체크하는 과정이 있음
4. 즉 사용자와 카드사 사이에서 일어나는 인증이 완료되면 PG도 인증이 끝났음을 알게 되는 과정이 있다
5. 인증완료가 확인되면 리다이렉트 등을 수행하고, 그 후 가맹점은 PG로 결제승인 API를 호출하게 된다.
또한 토스페이먼츠의 테스트 환경에서 개발자 도구의 네트워크 탭을 켜고 모든 HTTP 요청들을 수집해서 분석했었는데, 토스페이먼츠는 인증 과정에서
1. 결제창에서 사용자가 선택한 수단(카드사 포함)을 토스페이먼츠로 보내는 과정이 있음
- 결제금액, 카드사정보 등을 담아 보내는 결제 파라미터 관련 호출을 하는 과정이 있었습니다
2. 카드사별 인증창이 닫히면, 인증 완료를 토스페이먼츠에 알리는 과정이 있음
- 카드사 인증창이 닫힌 직후, 브라우저가 카드사 쪽 API로 인증 결과를 요청한 뒤 이어서 토스페이먼츠로 인증 관련 호출을 하는 과정이 있었습니다
3. 2번 과정이 정상적으로 끝나면 결제성공 url로 이동
를 하고 있었습니다. 물론 PG사별 결제 진행 방식엔 서로 차이가 있겠습니다만, 토스페이먼츠의 결제 흐름 분석을 바탕으로 저는 아래처럼 결제 흐름을 가정했습니다.
1. 사용자와 카드사 간 인증이 일어날 때, PG는 사용자가 어느 카드사를 선택했었는지 알게 된다 (= PG로 사용자가 고른 카드사 등을 전달하는 과정이 있다).

2. 또한 사용자와 카드사 간 인증이 일어날 때, 카드사는 어떠한 결제식별자에 어떤 카드가 사용되는지에 대한 정보를 저장한다.

3. 또한 사용자와 카드사 간 인증이 끝나면, 카드사에서 인증값(1회용 인증토큰)을 만들어주고 이 값은 PG에 저장된다.

4. 이후 가맹점 서버가 PG로 결제승인 API를 호출하면, PG는 1번에서 받은 카드사 정보를 통해 특정 카드사로 승인요청을 보내며, 이때 3번에서 받은 인증값을 사용한다.

5. 카드사는 PG로부터 승인요청을 받으면, 인증토큰과 2번에서 저장했던 결제식별자를 활용해 사용자가 골랐던 카드로 승인을 해주고, 응답으로 마스킹된 카드번호를 PG에게 준다.

이 내용들을 바탕으로, 제가 가졌던 의문인 "PG의 결제 승인 API를 호출할 땐 카드번호를 넘기지 않는데, 어떻게 사용자가 선택한 카드로 승인이 일어나지?" 에 대해 답을 해보면 다음과 같습니다.
1. 인증 결제에서는 PG는 사용자/가맹점으로부터 카드번호를 받지 않음
2. PG에서 어느 카드사로 라우팅해야 하는지는 인증 전에 확정됨
3. '결제 승인'은 인증토큰을 소모하는 행위임
3. mini-pg 설계
위에서 가정한 흐름들을 바탕으로, 제가 하고 있는 토이 PG인 mini-pg를 아래처럼 설계했습니다.
API
3개 API를 정의했습니다.
| Method | API | 설명 |
| POST | /api/payments/prepare | [ 결제준비 API ] - 요청 payload로 사용자가 선택한 카드사, 결제 금액, 수단 등을 받음 - 호출되면 PG는 READY 상태로 결제 데이터를 생성 |
| POST | /api/payments/{paymentId}/auth-complete | [ 인증완료 API ] - 호출되면 PG는 카드사로부터 해당 결제의 인증토큰을 받아옴 - 인증토큰을 정상적으로 받아오면 결제 상태를 IN_PROGRESS로 갱신 - 사용자 브라우저에서 카드사 인증창이 닫혀서 인증완료를 감지하면 브라우저에서 PG로 호출한다고 가정하는 API |
| POST | /api/payments/approve | [ 결제승인 API ] - 요청 payload로 결제식별자, 결제 금액, 수단 등을 받음 - 호출되면 PG는 결제 금액, 수단 등이 결제준비 API가 호출될 때 입력한 값과 동일한지 대조 - 그 후 PG가 카드사로 해당 결제의 승인을 요청 (인증토큰을 담아 요청) - 카드사에서 승인 응답을 받으면 결제 상태를 APPROVED로 갱신 - 카드사에서 실패 응답을 받으면 결제 상태를 ABORTED로 갱신 |
※ API별 요청 포맷, 응답 포맷은 생략합니다.
상태 변화
결제 상태의 변화는 아래처럼 약소화하여 정의했습니다. (mini-pg가 승인 위주로 다루기 때문입니다)

참고로 지금의 범위에서 EXPIRED, CANCELED 전이까지 구현하진 않았고, 해당 상태로의 전이들은 추후 mini-pg를 확장해나가며 구현할 예정입니다.
ERD
ERD는 아래처럼 설계했습니다.

※ 각 컬럼별 상세 설명은 생략합니다.
PAYMENT의 가맹점ID, 주문번호, 카드사, 총 결제금액 등은 결제준비 API가 호출될 때 등록되도록 했고, 이후 결제승인 API가 호출되면 이 값들과 대조하도록 했습니다. PAYMENT_AUTHENTICATION은 인증완료 API가 호출되면 PG가 카드사로부터 받아오는 인증토큰과 만료시간 등이 담기도록 했고, PAYMENT_CARD는 카드사가 승인 응답으로 준 값들을 저장하게 했습니다.
결제 흐름 상세 시퀀스
시퀀스는 아래와 같이 설계했으며, 제가 가정한 결제 흐름을 기반으로 설계했습니다.

참고로 "사용자가 결제창에서 인증을 하는 것"까지는 토이 범위를 넘어선다고 판단해서 만들지는 않았습니다. 다만 토큰 발급과 카드사 승인 요청 정도만 간단하게 처리해줄 수 있는 토이 카드사까지는 간단하게 만들어서 mini-pg에 활용했습니다.
앞으로 할 것들
처음에 말한 것처럼 이 프로젝트를 하게 된 계기는 회사에서 하고 있는 것과 다른 영역의 도메인을 느끼고 싶은 것도 있으나, 기술적으로 여러 요소를 고민해보는 경험을 해보고 싶은 것도 있습니다. 대표적으로 각 API들을 일부러 멱등하게 구현하지 않았는데, 이런 상황에서 멱등성 보장을 위한 여러 방법들을 직접 해보면서(물론 ai의 도움과 함께) 익히고 싶은 목적이 있습니다. 그 외에도 장애 시나리오들을 가정하고 서킷 브레이커 등을 적용해보는 것, 비동기 이벤트를 발행하고 다루기, 전부터 관심있던 분산 트레이싱을 적용 등을 이 mini-pg를 통해 시간을 내보며 해보려 합니다.
참고한 자료들
https://docs.tosspayments.com/resources/glossary/index
https://docs.tosspayments.com/reference
https://start.nicepay.co.kr/manual/quickguide/start.do
https://developers.kakaopay.com/docs/payment/online/single-payment
https://docs.pay.naver.com/docs/onetime-payment/payment/apply
https://developers.hectofinancial.co.kr/docs/api/pg/getting-started/01-full-flow