Vue와 Spring을 이용한 간편결제 적용기 1
1. 들어가며
다음 프로젝트에서 결제 개발을 맡을 예정이기에 투입 이전에 간편결제 샘플 작업을 해보려고 합니다.
우선 토스페이와 카카오페이만을 대상으로 샘플 작업을 해보겠습니다.
결제 방식은 결제 요청과 승인이 동시에 이루어지는 One-Factor 방식과
결제 요청과 승인 요청이 따로 분리되어 있는 Two-Factor 방식이 있습니다.
그 중 저는 대중적인 결제 방식인 Two-Factor 방식을 채택했습니다.
2. 토스페이 개발
토스페이 개발은 해당 문서를 참조했습니다.
먼저, 토스페이 개발을 위해서는 테스트 용 API Key가 필요합니다.
sk_test_w5lNQylNqa5lNQe013Nq
-
결제 요청
결제 프로세스 중 첫 번째인 [결제 요청] 단계입니다.
먼저, 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); } })
-
결제 승인
다음은 결제 프로세스 중 두 번째인 [결제 승인] 단계입니다.
앞서 윈도우 팝업에 결제창이 호출되면 사용자는 자신의 정보를 입력하고 토스페이 결제를 진행합니다.
사용자가 토스페이 결제를 완료하면 결제 요청 시 등록한 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
-
결제 요청
결제 프로세스 중 첫 번째인 [결제 요청] 단계입니다.
시작하기 앞서 카카오페이는 토스페이와는 다르게 애플리케이션을 생성해야 합니다.
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('결제 요청 실패') } });
-
결제 승인
다음은 결제 프로세스 중 두 번째인 [결제 승인] 단계입니다.
윈도우 팝업에 결제창이 호출되면 사용자는 자신의 정보를 입력하고 카카오페이 결제를 진행합니다.
사용자가 카카오페이에서 결제를 완료하면 결제 요청 시에 생성한 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; }
그럼 이제 지금껏 작성한 간편결제 코드를 리팩토링해보겠습니다.
-
INDEX
📕 개인 기록용 블로그입니다.
😊 오타나 잘못된 정보가 있을 경우 댓글이나 메일로 말씀해주시면 바로 수정하겠습니다! 😊
댓글남기기