토스페이먼츠 PG 연동

5win·2025년 1월 2일
0

시작하기 | 토스페이먼츠 개발자센터

주문을 접수하고 결제를 처리하기 위해 PG 연동을 시도해보고자 한다.

아임포트(iamport) 라는 통합 PG 연동 서비스도 있었으나, 살펴본 결과 토스 페이먼츠의 연동 가이드가 더 친절한 것 같기에 선택하게 되었다.

PG부터 결제 방법, 흐름, 구현까지 쭉 다룰 것이고 토스페이먼츠의 가이드를 참조해서 작성하도록 하겠다.


토스페이먼츠 PG

1. PG 란?

사실 이번 프로젝트에서 결제 구현 방법을 찾아보기 전까진 PG라는 단어도 알지 못했었다.

PG는 Payment Gateway의 준말로 “결제를 위한 관문”이라는 뜻이라고 한다.

이러한 PG를 제공해주는 기업 중에 하나로 토스페이먼츠가 있는 것이며 PG사 라고 부른다.

PG사는 결제기관인 카드사, 은행들로의 결제를 대신 담당해주는 역할을 하는데, 간단히 말하면 여러 결제기관에 대한 결제를 중앙 집중식으로 처리할 수 있도록 제공해주는 역할을 한다.

PG사는 결제를 대행해주는 대신 그에 대한 수수료를 받아 서비스는 유지하는 방식인 듯 하다.

2. 결제 흐름

토스페이먼츠에서 제공하는 결제 과정은 크게 요청, 인증, 승인 세 가지가 있다.

2-1. 결제 요청

우리가 흔히 결제하기 위해 “결제하기” 버튼을 클릭하면, 클라이언트가 해당 클릭의 이벤트로 토스페이먼츠 SDK의 결제 요청 메서드를 호출한다.

이 때 결제 요청 메서드의 파라미터로 주문번호(orderId), URL(successUrl, failUrl) 을 정의할 수 있다.

2-2. 구매자 정보 인증

구매자가 카드 정보를 입력하거나, 간편결제 앱을 통해 결제 정보를 불러온 뒤, 카드사에 정보를 전달한다.

카드사는 구매자 정보 확인을 통해 카드 소유자 인증 과정을 거치게 된다.

2-3. 인증 결과 확인

인증이 성공하면 토스페이먼츠는 결제 기록을 생성한 뒤, 결제 요청 메서드에서 파라미터로 전달했던 successUrl로 결제 정보를 담아 리다이렉트한다.

결제 정보에는 paymentKey, orderId와 같은 정보들이 포함된다.

https://oheat.com/success?paymentKey={PAYMENT_KEY}&orderId={ORDER_ID}&amount=100

2-4. 결제 승인

마지막으로 토스페이먼츠에서 인증된 결제가 승인되면 마무리된다.

서버는 successUrl로 전달된 파라미터 값들이 실제 결제 요청에 보냈던 값들과 동일한지 확인한 뒤, 결제 승인 API를 호출한다.

결제 승인 API를 호출하면 카드사에 결제 승인 요청이 전달되고, 카드사는 결제 금액을 구매자의 카드나 계좌에서 차감시킨뒤 응답을 하면서 마무리된다.

요청과 승인이라는 두 번의 작업을 따로 진행하는 이유는?
결론적으로 토스페이먼츠에서는 데이터 정합성과 연동 편의를 위해 요청과 승인을 따로 하는 방식으로 제공한다고 한다.
결제 요청과 승인을 한 번에 처리하면 오히려 복잡하고 웹훅 연동으로 인한 데이터 불일치 문제가 발생할 수 있기 때문이라고 한다.

2-5. PaymentKey란?

PaymentKey는 각 결제를 식별하는 값으로, 결제 인증이 완료되면 successUrl로 paymentKey가 전달된다.

결제 승인, 취소, 조회 시에 필요한 키값이기 때문에 DB에 저장하여 보관해야 한다.
그러나 어차피 시크릿 키가 없다면 paymentKey로 아무 것도 할 수 없기 때문에 외부에 노출되어도 상관은 없다. 따라서 그냥 잘 보관하기만 하자.

3. 결제 정보 검증하기

3-1. 결제 요청 전, 결제할 데이터 저장하기

결제를 요청하기 전에 먼저 구매자의 결제 정보를 DB에 저장해야 한다. 이는 결제 요청 전후의 데이터가 바뀌지 않았는지 확인하기 위함이다.

그리고 쿠폰, 적립금 기능이 존재한다면 최종 결제 금액을 계산하여 함께 저장하는 것이 안전하다. 구매자가 악의적으로 결제 요청 시 결제 금액을 수정할 수도 있기 때문이다.

3-2. 결제 승인 전, 승인할 데이터 검증하기

결제를 요청한 뒤 인증까지 성공했다면 이제 승인만 하면 결제 프로세스가 마무리된다.

하지만 데이터 정합성을 보장하기 위해 승인에 앞서 데이터 검증 과정이 필요하다.

이는 요청하기 전에 저장해놨던 데이터와 인증의 결과로 받은 데이터가 동일한지 검증하는 방식으로, 다음과 같은 방법으로 진행할 수 있다.

  1. successUrl로 전달되는 orderId 로 요청 전에 저장했던 임시 정보를 불러온다.
  2. 데이터가 동일한지 검증한다.
  3. 문제가 없다면 결제 승인을 요청한다.

토스페이먼츠 결제 연동 예제 실습

1. 결제 연동 플로우

연동하기 | 토스페이먼츠 개발자센터

토스페이먼츠 측에서 결제 연동 예제 코드를 제공하고 있다.

매우 친절하게 클라이언트, 서버의 기술에 따라 코드를 다양하게 제공하고 있어 실습해보기 좋은 환경이다.

일단 위와 같은 플로우로 결제가 진행되는데, 현재 나는 서버 API를 개발하는 것이 목적이므로 다음과 같은 방식으로 실습을 해보고자 한다.

  1. 예제 클라이언트 코드를 그대로 사용하여 프론트 테스트 환경 구축
  2. successUrl, failUrl 만 수정하여 토스페이먼츠로부터 결제정보를 전달 받는다.
  3. 결제 정보가 잘 전달되는지 출력하여 확인한다.
  4. 결제 승인 API를 호출하여 결제 프로세스를 마무리한다.

2. 예제 코드를 활용한 프론트 테스트 환경 구축

위에서 첨부한 링크의 프론트 코드를 통해 테스트 환경을 만들었다.

코드는 복붙한 뒤 다음과 같이 successUrl만 수정하였다.

 button.addEventListener("click", async function () {
      await widgets.requestPayment({
        orderId: "ZGBSxRhAU_Hgz26urk0pB",
        orderName: "토스 티셔츠 외 2건",
        // successUrl: window.location.origin + "/success.html",
        successUrl: "http://localhost:8080/api/v1/payments/toss",
        failUrl: window.location.origin + "/fail.html",
        customerEmail: "customer123@gmail.com",
        customerName: "김토스",
        customerMobilePhone: "01012341234",
      });
    });

index.html로 저장하였으므로 http://localhost:8080/ 로 접속하면 다음과 같은 화면이 잘 출력된다.

하지만 아직 successUrl에 해당하는 엔드포인트를 구현하지 않았으므로 결제를 하더라도 아무런 응답이 발생하지 않는다.

3. successUrl 엔드포인트 구현 및 결제 정보 확인

successUrl로 명시했던 /api/v1/payments/toss 엔드포인트를 구현해보자.

요청을 받을 수 있도록 매우 간단한 엔드포인트를 만들고, 받은 응답을 출력하고 200 OK를 반환하도록 했다.

(공식 문서에서 잘 보이지 않는데, successUrl로 POST가 아닌 GET 메서드로 요청이 전달된다)

다음으로 결제를 했더니 다음과 같이 정보가 잘 출력되는 것을 확인했다.

4. 결제 승인 API 호출하기

결제 정보를 서버에서 받는 것 까지 성공했으니, 이제 결제 승인을 할 차례다.

원래는 클라이언트가 결제를 하는 동시에 결제 정보를 서버에 저장하고, successUrl을 통해 받은 결제 정보와 동일한지 검증하는 과정이 필요하다.

일단 이 부분은 건너뛰고 결제 승인부터 성공시켜보자.

코어 API | 토스페이먼츠 개발자센터

연동하기 | 토스페이먼츠 개발자센터

위 링크로 들어가면 사진과 같이 결제 승인 API에 대한 문서가 있다.

결제 정보로 받은 값과 시크릿값을 함께 담아서 승인 요청을 하면 되는 것 같다.

컨트롤러에서 결제 정보 객체와 시크릿값을 만들어 RestClient로 요청을 보내고 응답을 받아 반환하도록 간단히 구현해보았다.

토스 앱에서는 결제 완료 표시와 함께 5만원이 결제됐다고 알림까지 왔다!

이로써 결제 승인이 완료됐다는 것을 확인할 수 있다.

그리고 클라이언트에도 응답이 잘 전달됐다.

하지만 body가 null이기 때문에, 이제 서버에서 응답 바디를 객체에 매핑하여 반환해주도록 변경해보자.

응답 바디 예시는 다음과 같다.

  • 응답 바디 예시
    {
      "mId": "tosspayments",
      "lastTransactionKey": "9C62B18EEF0DE3EB7F4422EB6D14BC6E",
      "paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
      "orderId": "a4CWyWY5m89PNh7xJwhk1",
      "orderName": "토스 티셔츠 외 2건",
      "taxExemptionAmount": 0,
      "status": "DONE",
      "requestedAt": "2024-02-13T12:17:57+09:00",
      "approvedAt": "2024-02-13T12:18:14+09:00",
      "useEscrow": false,
      "cultureExpense": false,
      "card": {
        "issuerCode": "71",
        "acquirerCode": "71",
        "number": "12345678****000*",
        "installmentPlanMonths": 0,
        "isInterestFree": false,
        "interestPayer": null,
        "approveNo": "00000000",
        "useCardPoint": false,
        "cardType": "신용",
        "ownerType": "개인",
        "acquireStatus": "READY",
        "receiptUrl": "https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&ref=PX",
        "amount": 1000
      },
      "virtualAccount": null,
      "transfer": null,
      "mobilePhone": null,
      "giftCertificate": null,
      "cashReceipt": null,
      "cashReceipts": null,
      "discount": null,
      "cancels": null,
      "secret": null,
      "type": "NORMAL",
      "easyPay": {
        "provider": "토스페이",
        "amount": 0,
        "discountAmount": 0
      },
      "country": "KR",
      "failure": null,
      "isPartialCancelable": true,
      "receipt": {
        "url": "https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&ref=PX"
      },
      "checkout": {
        "url": "https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout"
      },
      "currency": "KRW",
      "totalAmount": 1000,
      "balanceAmount": 1000,
      "suppliedAmount": 909,
      "vat": 91,
      "taxFreeAmount": 0,
      "metadata": null,
      "method": "카드",
      "version": "2022-11-16"
    }
    

응답 예제를 보면 날짜가 2024-02-13T12:18:14+09:00 같은 형식인데,
이 형식은 LocalDateTime이 아닌, OffsetDateTime 타입이다.

너무 기니까…일단 몇 개만 저장되도록 해보자.

약간의 이슈가 있다면, 이미 승인한 주문번호로는 아래와 같은 오류가 발생했다.

테스트용 결제라 상관 없을 줄 알았는데..아무래도 승인할 때마다 주문번호를 바꿔야하나?

토스페이먼츠 측에서 제공하는 코드를 보니 랜덤값을 생성하여 사용하도록 되어있었으므로 나도 그대로 사용했다.

const generateRandomString = () => window.btoa(Math.random()).slice(0, 20);

참고로 orderId는 다음 조건을 만족해야한다.

  1. 6자이상 64자 이하의 문자열
  2. 영문 대소문자, 숫자, -, _

다음과 같이 TossPaymentResponse 객체를 만들어 응답값을 매핑했고

클라이언트에도 응답 결과가 잘 나오는 것을 확인할 수 있었다!

0개의 댓글