카테고리:

업데이트:

1. 들어가며

다음 프로젝트에서 결제 개발을 맡을 예정이기에 투입 이전에 간편결제 샘플 작업을 해보려고 합니다.

우선 토스페이와 카카오페이만을 대상으로 샘플 작업을 해보겠습니다.

결제 방식은 결제 요청과 승인이 동시에 이루어지는 One-Factor 방식과

결제 요청과 승인 요청이 따로 분리되어 있는 Two-Factor 방식이 있습니다.

그 중 저는 대중적인 결제 방식인 Two-Factor 방식을 채택했습니다.

2. 토스페이 개발

토스페이 개발은 해당 문서를 참조했습니다.

먼저, 토스페이 개발을 위해서는 테스트 용 API Key가 필요합니다.

sk_test_w5lNQylNqa5lNQe013Nq

  1. 결제 요청

    결제 프로세스 중 첫 번째인 [결제 요청] 단계입니다.

    먼저, Vue 페이지에서 우리 서버로 결제 요청합니다.

    저는 axios를 커스터마이징한 Request를 사용했습니다.

     // Request.vue
       Request.POST('http://localhost:3000/api/easy-pay/request/toss').then(res => {
            
       });
    

    그리고 우리 서버에서는 다시 토스 서버에 결제 요청을 합니다.

    토스에서 제공하는 결제 요청 예제를 보면 아래와 같이 8가지 데이터를 담아 토스에 요청해야 합니다.

     curl https://pay.toss.im/api/v2/payments \
       -H "Content-Type: application/json" \
       -d '{
             "orderNo":"1",                                # 상품 주문번호
             "amount":25000,                               # 결제 금액
             "amountTaxFree":0,                            # 비과세 금액
             "productDesc":"T-Shirts",                     # 상품명
             "apiKey":"sk_test_w5lNQylNqa5lNQe013Nq",      # 상점 API Key
             "autoExecute":false,                          # 자동 승인 설정
             "retUrl":"http://YOUR-SITE.COM/ORDER-CHECK",  # 인증 완료 후 연결할 URL
             "retCancelUrl":"http://YOUR-SITE.COM/close"   # 결제 중단 시 사용자를 이동시킬 가맹점 페이지
           }'
    

    그리고 해당 데이터를 토스 서버에 HTTP 요청을 보내기 위해서는 다음과 같이 작성할 수 있습니다.

     @RestController
     @RequestMapping("/easy-pay")
     public class EasyPayController {
          
       // 결제 요청 API
       @PostMapping("/request/toss")
       public JSONObject createTossPay() {
         // 주문 번호 생성
         LocalDateTime today = LocalDateTime.now();
         String orderNo = today.format(DateTimeFormatter.BASIC_ISO_DATE) + (int)(Math.random() * 10000);
    
         try {
           // Connection
           URL url = new URL("https://pay.toss.im/api/v2/payments");
           URLConnection connection = url.openConnection();
           connection.addRequestProperty("Content-Type", "application/json");
           connection.setDoOutput(true);
           connection.setDoInput(true);
    
           // 데이터 생성
           org.json.simple.JSONObject jsonBody = new JSONObject();
           jsonBody.put("orderNo", orderNo);
           jsonBody.put("amount", 25000);
           jsonBody.put("amountTaxFree", 5);
           jsonBody.put("productDesc", "T-Shirts");
           jsonBody.put("apiKey", "sk_test_w5lNQylNqa5lNQe013Nq");
           jsonBody.put("autoExecute", false);
           jsonBody.put("retUrl", "http://localhost:3000/easy-pay/order-check/toss");
           jsonBody.put("retCancelUrl", "http://localhost:3000/easy-pay/close");
    
           // 요청
           BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream());
    
           bos.write(jsonBody.toJSONString().getBytes(StandardCharsets.UTF_8));
           bos.flush();
           bos.close();
    
           // JSONObject에 매핑
           BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
           JSONParser jsonParser = new JSONParser();
           JSONObject result = (JSONObject) jsonParser.parse(br.readLine());
    
           br.close();
           return result;
         } catch (Exception e) {
           return null;
         }
       }
     }
    

    요청이 성공하면 checkoutPage과 payToken을 받고 이를 다시 Vue 페이지에 요청 값으로 돌려줍니다.

    checkoutPage는 결제창 호출을 위한 URL, payToken은 결제 번호입니다.

    결제 번호는 승인 요청 시 필요하므로 Vue 중앙 저장소(vuex, pinia)에 저장하고

    결제창 호출을 위한 URL을 윈도우 팝업을 통해 호출합니다.

     // Request.vue
       Request.POST('http://localhost:3000/api/easy-pay/request/toss').then(res => {
         const { code, payToken, checkoutPage, msg } = res;
    
         if (code === 0) {
           store.dispatch('payStore/setPayToken', payToken);
           window.open(checkoutPage, 'toss', 'width=500,height=600');
         } else {
           alert(msg);
         }
       })
    
  2. 결제 승인

    다음은 결제 프로세스 중 두 번째인 [결제 승인] 단계입니다.

    앞서 윈도우 팝업에 결제창이 호출되면 사용자는 자신의 정보를 입력하고 토스페이 결제를 진행합니다.

    사용자가 토스페이 결제를 완료하면 결제 요청 시 등록한 retUrl로 Redirect됩니다.

    그럼, 중앙 저장소에 저장했던 payToken 값을 가져와 우리 서버에 승인 요청을 합니다.

    // Process.vue
     Request.POST('http://localhost:3000/api/easy-pay/approve/toss', {
       payToken: store.getters['payStore/getPayToken']
     }).then(res => {
          
     })
    

    그리고 우리 서버는 다시 토스 서버에 승인 요청을 합니다.

    토스에서 제공하는 결제 승인 예제를 보면 아래와 같이 2가지 값을 담아 토스에 요청해야 합니다.

     curl "https://pay.toss.im/api/v2/execute" \
       -H "Content-Type: application/json" \
       -d '{
             "apiKey":"sk_test_w5lNQylNqa5lNQe013Nq", # 상점 API Key
             "payToken":"example-payToken",           # 결제 고유 번호
           }'
    

    그리고 해당 데이터를 토스 서버에 HTTP 요청을 보내기 위해서는 다음과 같이 작성할 수 있습니다.

     @RestController
     @RequestMapping("/easy-pay")
     public class EasyPayController {
    
       // 결제 요청 API
       @PostMapping("/request/toss")
       public JSONObject createTossPay() { ... }
    
       // 결제 승인 API
       @PostMapping("/approve/toss")
       public JSONObject approveTossPay(@RequestBody TossPay tossPay) {
         try {
           // Connection
           URL url = new URL("https://pay.toss.im/api/v2/execute");
           URLConnection connection = url.openConnection();
           connection.addRequestProperty("Content-Type", "application/json");
           connection.setDoOutput(true);
           connection.setDoInput(true);
    
           // 데이터 생성
           JSONObject jsonBody = new JSONObject();
           jsonBody.put("apiKey", "sk_test_w5lNQylNqa5lNQe013Nq");
           jsonBody.put("payToken", tossPay.getPayToken());
    
           // 요청
           BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream());
    
           bos.write(jsonBody.toJSONString().getBytes(StandardCharsets.UTF_8));
           bos.flush();
           bos.close();
    
           // JSONObject 매핑
           BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
           JSONParser jsonParser = new JSONParser();
           JSONObject result = (JSONObject) jsonParser.parse(br.readLine());
    
           br.close();
           return result;
         } catch (Exception e) {
           return null;
         }
       }
     }
    

    토스 서버의 승인 요청이 성공하면 카드 번호, 카드사 등 결제 내역을 받습니다.

    그 후, 결제가 완료되었기 때문에 팝업을 닫고 결제 완료 페이지로 이동합니다.

    // Process.vue
     switch (type) {
       case 'toss':
         Request.POST('http://localhost:3000/api/easy-pay/approve/toss', {
           payToken: store.getters['payStore/getPayToken']
         }).then(res => {
           const { code, msg } = res;
    
           store.dispatch('payStore/setPayToken', '');
           if (code === 0) {
             window.opener.location.href=`/easy-pay/complete?${objToParam(res)}`;
             window.close();
           } else {
             alert(`주문 실패 (사유: ${msg})`);
           }
         })
    
         break;
     }
    

3. 카카오페이 개발

카카오페이 개발은 해당 문서를 참조했습니다.

카카오페이도 마찬가지로 테스트 용 API Key가 필요합니다.

TC0ONETIME

  1. 결제 요청

    결제 프로세스 중 첫 번째인 [결제 요청] 단계입니다.

    시작하기 앞서 카카오페이는 토스페이와는 다르게 애플리케이션을 생성해야 합니다.

    Kakao Developers → 내 애플리케이션

    생성이 완료되었으면 인증 토큰으로 사용할 Admin 키 확인과 사이트 도메인을 등록해야 합니다.

    먼저, Admin 키를 확인해봅시다.

    내 애플리케이션 → 앱 설정 → 요약 정보 → Admin 키 확인

    그리고 사이트 도메인을 등록합시다.

    내 애플리케이션 → 앱 설정 → 플랫폼 → Web

    그럼 이제 개발을 시작해보겠습니다.

    먼저, Vue 페이지에서 우리 서버로 결제 요청합니다.

     // Request.vue
       Request.POST('http://localhost:3000/api/easy-pay/request/kakao').then(res => {
            
       });
    

    그리고 우리 서버에서는 다시 카카오 서버에 결제 요청을 합니다.

    카카오에서 제공하는 결제 요청 예제를 보면

    아래와 같이 1개의 인증 토큰과 인코딩한 10가지 데이터를 담아 카카오에 요청해야 합니다.

     curl -v -X POST "https://kapi.kakao.com/v1/payment/ready" \
             -H "Authorization: KakaoAK ${SERVICE_APP_ADMIN_KEY}" \                    # 인증 토큰
             --data-urlencode "cid=TC0ONETIME" \                                       # 가맹점 코드
             --data-urlencode "partner_order_id=partner_order_id" \                    # 주문 번호
             --data-urlencode "partner_user_id=partner_user_id" \                      # 회원 ID
             --data-urlencode "item_name=T-Shirts" \                                   # 상품명               
             --data-urlencode "quantity=1" \                                           # 상품 수량
             --data-urlencode "total_amount=2200" \                                    # 상품 총액
             --data-urlencode "tax_free_amount=0" \                                    # 상품 비과세 금액
             --data-urlencode "approval_url=https://developers.kakao.com/success" \    # 결제 성공 시 Redirect URL
             --data-urlencode "fail_url=https://developers.kakao.com/fail" \           # 결제 실패 시 Redirect URL
             --data-urlencode "cancel_url=https://developers.kakao.com/cancel"         # 결제 취소 시 Redirect URL
    

    그리고 해당 데이터를 카카오 서버에 HTTP 요청을 보내기 위해서는 다음과 같이 작성할 수 있습니다.

     @RestController
     @RequestMapping("/easy-pay")
     public class EasyPayController {
          
       // 결제 요청 API
       @PostMapping("/request/kakao")
       public JSONObject createKakaoPay() {
         try {
           // 주문 번호 생성
           LocalDateTime today = LocalDateTime.now();
           String orderNo = today.format(DateTimeFormatter.BASIC_ISO_DATE) + (int)(Math.random() * 10000);
           Strign userId = "test1";
    
           // Connection
           URL url = new URL("https://kapi.kakao.com/v1/payment/ready");
           HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    
           connection.setRequestProperty("Authorization", "KakaoAK " + "KAKAO-TOKEN");
           connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
           connection.setRequestMethod("POST");
           connection.setDoOutput(true);
           connection.setDoInput(true);
    
           // 데이터 생성                    
           String data = URLEncoder.encode("cid", "UTF-8") + "=" + URLEncoder.encode("TC0ONETIME", "UTF-8") + "&";
           data += URLEncoder.encode("partner_order_id", "UTF-8") + "=" + URLEncoder.encode(orderNo, "UTF-8") + "&";
           data += URLEncoder.encode("partner_user_id", "UTF-8") + "=" + URLEncoder.encode(userId, "UTF-8") + "&";
           data += URLEncoder.encode("item_name", "UTF-8") + "=" + URLEncoder.encode("T-Shirts", "UTF-8") + "&";
           data += URLEncoder.encode("quantity", "UTF-8") + "=" + URLEncoder.encode("10", "UTF-8") + "&";
           data += URLEncoder.encode("total_amount", "UTF-8") + "=" + URLEncoder.encode("1000", "UTF-8") + "&";
           data += URLEncoder.encode("tax_free_amount", "UTF-8") + "=" + URLEncoder.encode("0", "UTF-8") + "&";
           data += URLEncoder.encode("approval_url", "UTF-8") + "=" + URLEncoder.encode("http://localhost:3000/easy-pay/order-check/kakao", "UTF-8") + "&";
           data += URLEncoder.encode("cancel_url", "UTF-8") + "=" + URLEncoder.encode("http://localhost:3000/easy-pay/close", "UTF-8") + "&";
           data += URLEncoder.encode("fail_url", "UTF-8") + "=" + URLEncoder.encode("http://localhost:3000/easy-pay/fail/kakao", "UTF-8");
    
           // 요청
           DataOutputStream dataOutputstr = new DataOutputStream(connection.getOutputStream());
           dataOutputstr.writeBytes(data);
           dataOutputstr.flush();
           dataOutputstr.close();
    
           // JSONObject 매핑
           BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
           JSONParser jsonParser = new JSONParser();
           JSONObject result = (JSONObject) jsonParser.parse(br.readLine());
    
           result.put("orderNo", orderNo);
    
           br.close();
           return result;
    
         } catch (Exception e) {
           return null;
         }
       }
     }
    

    요청이 성공하면 next_redirect_pc_url, tid를 받고 이를 다시 Vue 페이지에 요청 값으로 돌려줍니다.

    next_redirect_pc_url은 결제창 호출을 위한 URL, tid는 결제 번호입니다.

    그 밖에 orderNo는 주문 번호, userId는 회원 ID입니다.

    결제 번호, 주문 번호, 회원 ID는 승인 요청 시 필요하므로 Vue 중앙 저장소(vuex, pinia)에 저장하고

    결제창 호출을 위한 URL을 윈도우 팝업을 통해 호출합니다.

    // Request.vue
     Request.POST('http://localhost:3000/api/easy-pay/request/kakao').then(res => {
       const { next_redirect_pc_url, tid, orderNo, userId } = res;
    
       if (!!next_redirect_pc_url) {
         store.dispatch('payStore/setPayToken', tid);
         store.dispatch('payStore/setOrderNo', orderNo);
         store.dispatch('payStore/setUserId', userId);
         window.open(next_redirect_pc_url, 'kakao', 'width=500, height=600');
       } else {
         alert('결제 요청 실패')
       }
     });
    
  2. 결제 승인

    다음은 결제 프로세스 중 두 번째인 [결제 승인] 단계입니다.

    윈도우 팝업에 결제창이 호출되면 사용자는 자신의 정보를 입력하고 카카오페이 결제를 진행합니다.

    사용자가 카카오페이에서 결제를 완료하면 결제 요청 시에 생성한 approval_url로 Redirect됩니다.

    Redirect될 때 pg_token 값을 함께 주는데 이는 승인 인증 토큰으로 승인 시에 꼭 필요한 정보입니다.

    받은 데이터를 종합해 다시 우리 서버로 승인 요청을 합니다.

    // Process.vue
     Request.POST('http://localhost:3000/api/easy-pay/approve/kakao', {
       payToken: query.pg_token,
       tid: store.getters['payStore/getPayToken'],
       orderNo: store.getters['payStore/getOrderNo'],
       userId: store.getters['payStore/getUserId']
     }).then(res => {
    
     });
    

    그리고 우리 서버는 다시 카카오 서버에 승인 요청을 합니다.

    카카오에서 제공하는 승인 요청 예제를 보면

    아래와 같이 1개의 인증 토큰과 urlencode를 한 5가지 데이터를 담아 카카오에 요청해야 합니다.

     curl -v -X POST "https://kapi.kakao.com/v1/payment/approve" \
             -H "Authorization: KakaoAK ${SERVICE_APP_ADMIN_KEY}' \      # 인증 토큰
             --data-urlencode "cid=TC0ONETIME" \                         # 가맹점 코드
             --data-urlencode "tid=T1234567890123456789" \               # 결제 번호
             --data-urlencode "partner_order_id=partner_order_id" \      # 주문 번호
             --data-urlencode "partner_user_id=partner_user_id" \        # 회원 ID
             --data-urlencode "pg_token=xxxxxxxxxxxxxxxxxxxx"            # 결제승인 요청을 인증하는 토큰
    

    그리고 해당 데이터를 카카오 서버에 HTTP 요청을 보내기 위해서는 다음과 같이 작성할 수 있습니다.

     @RestController
     @RequestMapping("/easy-pay")
     public class EasyPayController {
          
       // 결제 요청 API
       @PostMapping("/request/kakao")
       public JSONObject createKakaoPay() {}
    
       // 결제 승인 API
       @PostMapping("/approve/kakao")
       public JSONObject approveKakaoPay(@RequestBody KakaoPay kakaoPay) {
         try {
           // Connection
           URL url = new URL("https://kapi.kakao.com/v1/payment/approve");
           HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    
           connection.setRequestProperty("Authorization", "KakaoAK " + "KAKAO-TOKEN");
           connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
           connection.setRequestMethod("POST");
           connection.setDoOutput(true);
           connection.setDoInput(true);
    
           // 데이터 생성
           String data = URLEncoder.encode("cid", "UTF-8") + "=" + URLEncoder.encode("TC0ONETIME", "UTF-8") + "&";
           data += URLEncoder.encode("tid", "UTF-8") + "=" + URLEncoder.encode(kakaoPay.getTid(), "UTF-8") + "&";
           data += URLEncoder.encode("partner_order_id", "UTF-8") + "=" + URLEncoder.encode(kakaoPay.getOrderNo(), "UTF-8") + "&";
           data += URLEncoder.encode("partner_user_id", "UTF-8") + "=" + URLEncoder.encode("test1", "UTF-8") + "&";
           data += URLEncoder.encode("pg_token", "UTF-8") + "=" + URLEncoder.encode(kakaoPay.getPayToken(), "UTF-8");
    
           // 요청
           DataOutputStream dataOutputstr = new DataOutputStream(connection.getOutputStream());
           dataOutputstr.writeBytes(data);
           dataOutputstr.flush();
           dataOutputstr.close();
    
           // JSONObject 매핑
           BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
           JSONParser jsonParser = new JSONParser();
           JSONObject result = (JSONObject) jsonParser.parse(br.readLine());
    
           result.put("orderNo", orderNo);
    
           br.close();
           return result;
    
         } catch (Exception e) {
           return null;
         }
     }
    

    카카오 서버의 승인 요청이 성공하면 카드 번호, 카드사 등 결제 내역을 받습니다.

    그 후, 결제가 완료되었기 때문에 팝업을 닫고 결제 완료 페이지로 이동합니다.

    // Process.vue
     switch (type) {
       case 'kakao':
         Request.POST('http://localhost:3000/api/easy-pay/approve/kakao', {
           payToken: query.pg_token,
           tid: store.getters['payStore/getPayToken'],
           orderNo: store.getters['payStore/getOrderNo'],
           userId: store.getters['payStore/getUserId']
         }).then(res => {
           store.dispatch('payStore/setPayToken', '');
           store.dispatch('payStore/setOrderNo', '');
           store.dispatch('payStore/setUserId', '');
           window.opener.location.href=`/easy-pay/complete?${objToParam(res)}`;
           window.close();
         });
    
         break;
     }
    

그럼 이제 지금껏 작성한 간편결제 코드를 리팩토링해보겠습니다.

            
              📕 개인 기록용 블로그입니다.
              😊 오타나 잘못된 정보가 있을 경우 댓글이나 메일로 말씀해주시면 바로 수정하겠습니다! 😊
          

댓글남기기