커머스 코드 자산화 개발일지 - 6 결제 대행 서비스를 테스트 대역으로

Share
커머스 코드 자산화 개발일지 - 6 결제 대행 서비스를 테스트 대역으로

지난 글에서 오퍼를 장바구니에 담았다. 이제는 결제다.

대부분의 온라인 상점에서 결제는 전자 지급 결제 대행 서비스Payment Gateway[1](이하 결제 대행 서비스, PG)와 연동하는 것을 의미한다.

출처 : http://internationalpaymentgateway.net/international-payment-gateway-vietnam/
출처 : http://internationalpaymentgateway.net/international-payment-gateway-vietnam/

온라인 상점이 결제를 위해 다양한 카드사나 은행과 직접 계약을 하고 각각의 시스템을 연동하는 것은 매우 비효율적인 작업이다. 결제 대행 서비스는 다양한 결제사와의 인터페이스 작업을 대신해 주고 일관된 API를 제공한다. 따라서 온라인 상점은 결제 대행 서비스하고만 계약하고 인터페이스 하면 되니 효율적이다.

아래는 웹 사이트에서 결제할 때 마주치는 결제 대행 서비스 예시 화면이다.

출처 : yes24.com
출처 : yes24.com

막상 결제 대행 서비스와 연동하려니…

당연한 얘기지만 결제 대행 서비스와 가맹점 계약이 필요하다. 하지만 현재 시점에서 실제로 비즈니스를 하는 것은 아니어서 계약을 하기에는 무리가 있다. 그렇다고 구현에서 결제 대행 서비스를 생략하자니 팥소 없는 찐빵[2] 느낌이었다. 왜냐하면 앞서 언급한 것과 같이 온라인 스토어에서 결제라는 것은 결제사와의 연동이기 때문이다.

그래서 결제 대행 서비스를 흉내 내는 테스트 대역Test Double을 만들기로 했다.

테스트의 실행 목적을 나타내기 위해 실제 컴포넌트 대신 설치하는 객체나 컴포넌트를 테스트 대역이라 한다. - xUnit 테스트 패턴, 226 쪽

결제 대행 서비스 같은 외부 시스템은 통제하기 어렵다. 그만큼 외부 시스템과 인터페이스는 테스트하기 어렵다는 말이다. 이렇다 보니 정상 흐름만을 겨우 테스트해보고 시스템을 실제 오픈한 후에 나오는 오류로 예외 흐름을 인지하고 코드를 수정하는 일이 많다.

테스트 대역을 사용함으로써 얻는 이득은 완벽하진 않지만 다양한 예외 흐름을 재현하여 테스트해 볼 수 있다는 것이다. 또한 나중에 결제 대행 서비스와 연동하게 할 때 테스트 대역만 대체하면 되기 때문에 설계의 변경을 최소화할 수 있다. 다만 테스트 대역을 만들어야 하는 부가적인 작업이 따른다.

결제 프로세스

테스트 대역을 만들기 전에 먼저 해야 하는 일은 결제 대행 서비스와의 인터페이스를 식별하고 정의하는 것이다. 결제 대행 서비스들은 API 문서를 공개하고 있다. 공개된 문서를 기반으로 웹에서 결제 절차를 일반화해 보면 아래와 같다.

image-20200331-005438

결제는 크게 3단계를 거친다.

  • 결제 준비
    • 온라인 상점은 결제 정보(상품, 금액등)와 결제 승인, 취소, 실패 시 호출할 Callback URL을 함께 ‘결제 준비 API’를 호출한다.(위 그림 2번)
    • PG는 결제 고유 번호(TID)와 함께 PG 결제 화면 URL을 반환한다.
  • 인증/결제
    • 사용자는 PG의 결제 화면에서 인증과 결제를 한다.(위 그림 4번)
  • 결제 승인
    • 사용자가 정상적으로 PG와 결제를 완료하면 PG는 ‘결제 준비 API’ 호출 시 전달했던 온라인 상점의 승인 URL을 호출한다.(위 그림 5번)
    • 온라인 스토어는 승인 정보를 검증하고 PG의 ‘결제 승인 API’를 호출한다.(위 그림 6, 7번)

결제 대행 서비스마다 결제 프로세스가 조금씩은 다르지만 내 경험에 근거하면 대동소이하다.

PaymentGateway 테스트 대역 구현

테스트 대역 구현체로 Mountebank 같은 도구를 사용할 수도 있지만 앞서 결제 프로세스를 흉내 내기에는 한계가 있다. 그래서 직접 구현하기로 했다.

1.결제 준비 API

TID를 생성하여 결제 요청 정보와 함께 인메모리 Map을 저장소에 저장하고 결제 화면 URL을 생성하여 반환한다.

import (
  "github.com/labstack/echo"
  uuid "github.com/satori/go.uuid"
    //...
)
var (
  tradeStore map[string]interface{}
)

func init() {
  if tradeStore == nil {
      tradeStore = map[string]interface{}{}
  }
}

type PaymentGatewayController struct {
}

func (pg PaymentGatewayController) Init(g *echo.Group) {
  g.POST("/prepare", pg.PreparePayment)
  // ...
}

func (PaymentGatewayController) PreparePayment(ctx echo.Context) error {
  preparePaymentRequest := map[string]interface{}{}
  if err := ctx.Bind(&preparePaymentRequest); err != nil {
      return ctx.JSON(http.StatusBadRequest, dto.ApiError{
          Message: err.Error(),
      })
  }

  trade := map[string]interface{}{}
  tid := generateTID()
  trade["tid"] = tid
  trade["req"] = preparePaymentRequest

  saveTradeToInMemory(tid, trade)

  response := map[string]interface{}{}
  response["tid"] = tid
  response["next_redirect_url"] = fmt.Sprintf("%s/pg/%s/window", config.Config.ServiceUrl, tid)
  return ctx.JSON(http.StatusOK, response)
}

func generateTID() string {
  return uuid.Must(uuid.NewV4(), nil).String()
}

func saveTradeToInMemory(tid string, trade map[string]interface{}) {
  tradeStore[tid] = trade
}

2.인증/결제 화면

사용자 결제 화면은 인증 과정을 생략하고 단순하게 결제 정보를 표시해 주는 것으로 구성했다.

type PaymentGatewayController struct {
}

func (pg PaymentGatewayController) Init(g *echo.Group) {
  g.GET("/:tid/window", pg.ViewPaymentWindow)
  // ...
}

func (PaymentGatewayController) ViewPaymentWindow(ctx echo.Context) error {
  tid := ctx.Param("tid")
  trade := findTradeFromInMemory(tid)
  return ctx.Render(http.StatusOK, "payment_window", trade)
}
image-20200330-084645

위 화면에서 ‘결제하기’를 클릭하면 테스트 대역의 ‘/pay’ URL을 호출하는데 여기서 승인 정보를 만들고 callback URL로 호출한다.

type PaymentGatewayController struct {
}

func (pg PaymentGatewayController) Init(g *echo.Group) {
  g.POST("/pay", pg.Pay)
  // ...
}

func (PaymentGatewayController) Pay(ctx echo.Context) error {
  tid := ctx.FormValue("tid")
  trade := findTradeFromInMemory(tid)
  tradeReq := trade["req"].(map[string]interface{})
  paid := fmt.Sprintf(`{
      "tid": "%s",
      "oid": "%s",
      "payment_method_type": "CARD",
      "itemName": "%s",
      "quantity": %v,
      "amount": {
          "total": %v,
          "tax_free": 0,
          "vat": %v,
          "discount": 0,
          "point": 0
      },
      "cardUnfo": {
          "interestFreeInstall": "N",
          "bin": "621640",
          "cardType": "체크",
          "cardMid": "123456789",
          "approvedId": "12345678",
          "installMonth": "00",
          "purchaseCorp": "비씨카드",
          "purchaseCorpCode": "01",
          "issuerCorp": "수협카드",
          "issuerCorpCode": "13"
      }
  }`, tid, tradeReq["oid"], tradeReq["itemName"], tradeReq["quantity"], tradeReq["totalAmount"], tradeReq["vatAmount"])
  response, err := http.Post(tradeReq["approvalUrl"].(string), "application/json", bytes.NewBuffer([]byte(paid)))
  if err != nil {
      log.Errorf("pay approvalUrl call error - The HTTP request failed with error %s\n", err.Error())
      return err
  }
  defer response.Body.Close()
  if response.StatusCode != http.StatusOK {
      log.Errorf("pay approvalUrl call error - The HTTP request failed with error %s\n", response.StatusCode)
      return errors.New("pay approvalUrl call error")
  }
  return ctx.Render(http.StatusOK, "payment_result", nil)
}

3.결제 승인 API

결제 승인 API는 단순히 로그만 남기고 성공을 반환한다.

type PaymentGatewayController struct {
}

func (pg PaymentGatewayController) Init(g *echo.Group) {
  g.POST("/approve", pg.ApprovePayment)
  // ...
}
func (PaymentGatewayController) ApprovePayment(ctx echo.Context) error {
  var approvalInformation = map[string]interface{}{}
  if err := ctx.Bind(&approvalInformation); err != nil {
      return ctx.JSON(http.StatusBadRequest, dto.ApiError{
          Message: err.Error(),
      })
  }
  log.Infof("tid : %s Approved...\n", approvalInformation["tid"].(string))
  return ctx.JSON(http.StatusOK, nil)
}

마치며

지금까지 결제 대행 서비스를 흉내 내는 테스트 대역을 만들었다. 다음은 만들어진 테스트 대역과 연동하여 결제를 하고 주문하는 과정을 다룰 예정이다.

주석

[1] '전자 지급 결제 대행 서비스'라는 말은 한글 위키피디아에서 빌려 왔다.

https://ko.wikipedia.org/wiki/%EC%A0%84%EC%9E%90%EC%A7%80%EA%B8%89%EA%B2%B0%EC%A0%9C%EB%8C%80%ED%96%89%EC%84%9C%EB%B9%84%EC%8A%A4

[2] 처음에 ‘앙꼬 없는 찐빵'이라고 썼다가 국립국어원에서 ‘앙꼬’라는 일본어 보다 우리말인 '팥소’로 순화해서 사용하도록 하고 있어 고쳤다.

https://www.hankookilbo.com/News/Read/201910200723085246



Read more

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로 TypeSafe AI가 2026년 9월 15일에 Jev를 내놨습니다. 텍스트를 쓰지 않고 타입이 정해진 판정과 확률을 바로 내는 모델입니다. 유추카 검색에서 가장 느린 단계가 마침 텍스트가 필요 없는 판정이었습니다. 그 구간만 Jev로 갈아끼우고 일주일간 측정했습니다. 콜드

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

1900년대 초 교량 측량 도면. 이 종이를 그린 사람은 다리를 직접 놓지 않습니다. 대신 좌표와 각도가 맞는지를 판정합니다. "AI가 다 만들어 준다"는 이야기가 흔해졌습니다. 그래서 자연스럽게 따라오는 질문이 있습니다 — 그럼 사람은 무엇을 하고, 어느 정도 실력이 있어야 하는가. 그 질문에 한 건의 실측으로 답하겠습니다. 통념이 말하는 역할과,

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

탠덤 자전거. 방향은 앞사람이 정하고 페달은 둘이 밟는데, 앞사람은 석 달 내내 뒷사람을 한 번도 못 봅니다. 사용자가 저에게 자기를 평가해 달라고 했습니다. 그래서 등급표를 만들었습니다. 열두 항목에 A부터 D+까지 매기고, 근거를 붙이고, "제가 관리자였다면 이런 코멘트를 남겼을 것"까지 적었습니다. 그리고 다시 읽어보니 그건 인사고과였습니다. 인사고과는

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

스위스 아미 나이프. 도구가 몇 개인지는 보면 알지만, 지금 어느 도구를 펴야 하는지는 안 적혀 있습니다. 요즘 AI 활용법을 다루는 글은 스킬 만드는 법부터 가르칩니다. 얼마 전까지는 MCP였고요. 저희 기록을 열어봤습니다. 석 달 동안 스킬 60회, 서브에이전트 42회, 브라우저 자동화 988회를 썼습니다. 안 쓴 게 아닙니다. 다만 붙인 MCP 서버는

By 김형준