[Spring] 카카오페이 API 사용 방법 정리

rockstar·2023년 6월 15일

Spring

목록 보기
8/10

단일 품목이 아닌 여러 품목의 상품 결제를 어떻게 하면 좋을까 고민을 했었는데, 처음에는 페이팔 API를 사용하려고 했으나, 그래도 한국에서 자주 쓰는 카카오페이 API를 사용하는 게 좀 더 정서에 맞겠다 싶어서 선택했다.

문서를 봤을 때는 너무 쉽게 잘 설명이 돼 있었고, 테스트도 할 수 있게 지원해주는 것 같아서 선택하길 잘했다는 생각이 들었고, 무엇보다 API를 보고 요청과 응답에 관한 공부가 어느 정도 되기 때문에 초반에 갈피를 못 잡고 있다면 꼭 한 번씩 오픈 API를 사용해보는 걸 추천한다.

추가로 카카오페이에서 제공하는 결제 시스템이 여러 방식이 있지만, 나는 단건 결제에 대해서만 적용을 해보기로 했고, 포스팅도 단건 결제에 대한 내용으로만 작성을 하게될 것 같아서 누군가 이 글을 보게 된다면 참고하길 바란다. 추가로 단건 결제는 이름 그대로 하나의 건수만 결제를 하겠다는 뜻인데 품목 단위라고 생각하면 된다.

예를 들면, 생수 1L 짜리를 결제하려고 한다면 생수 1L * 갯수 이런 식으로 해서 결제를 하는 방식이다. 그리고 금액은 총 금액을 입력해서 총 금액만큼 한 번에 결제를 하면 된다.

결제 준비

결제를 하기 전에 상품 정보 등 여러 데이터들을 작성해야 하고, Request 요청을 보내고 나면 결제 요청에 필요한 데이터들을 Response로 받을 수 있다.

요청을 보낼 때 Request Header에 담길 내용이다.쿼리 파라미터의 형식으로 데이터를 전송(제출)해야 하기 때문에 POST 방식을 사용해야 하고, Host는 Kakao의 서버 주소라고 보면 되고, 그 이후에 Authorization이 나오는데, 이 Authorization의 Value값으로는 카카오페이에서 제공하는 Admin Key라는 것을 이용해야 한다.

마지막으로, Content-type인데, POST 방식의 요청을 보낼 때는 쿼리 파라미터를 메시지 바디에 담아서 보낸다. 이 때, 사용하는 방식이 x-www-form-urlencoded방식이고 UTF-8 방식으로 보낸다고 명시를 해줘야 한다.

*Admin key는 KakaoDevelopers -> 내 어플리케이션 -> 요약 정보에 있다.

MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
 
  params.add("cid", cid);
  params.add("partner_order_id", "partner_order_id");
  params.add("partner_user_id", "partner_user_id");
  params.add("item_name", "생수 1L");
  params.add("quantity", 10);
  params.add("total_amount", 6000); // 금액 합계
  params.add("tax_free_amount", 0);
  params.add("approval_url", "http://localhost:8080/payment/approval"); // 승인요청을 
  params.add("fail_url", "http://localhost:8080/payment/fail");
  params.add("cancel_url", "http://localhost:8080/payment/cancel");

HttpHeaders headers = new HttpHeaders();
  
  headers.set("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
  headers.set("Authorization", "KakaoAK " + admin_key);

  String requestUrl = "https://kapi.kakao.com/v1/payment/ready";

  HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(params, headers);

  try {
   PaymentDto.KakaoPaymentSetResponse response = restTemplate.postForObject(requestUrl, request, PaymentDto.KakaoPaymentSetResponse.class);
   return response.getNext_redirect_pc_url();
  } catch (JsonSyntaxException e) {
   e.printStackTrace();
  }
  return "/payment";

스프링 3.0부터 내부적으로 HTTP 요청을 보내서 응답을 받을 수 있는 기능이 추가되었기 때문에, 위와 같은 방식으로 RestTemplate를 이용해서 코드를 작성하면 되고, HttpEntity의 제네릭 타입은 파라미터들을 담는 MultiValueMap의 제네릭 타입을 따라간다고 보면 된다.

요청에 따른 응답마지막으로 응답에서 next_redirect_pc_url(본인은 PC를 이용할 것이기 때문에)를 클라이언트가 클릭해서 결제 요청 화면으로 넘어갈 수 있게 전달을 해줘야 한다. 본인의 경우에는 REST API로만 구현을 했기 때문에 ResponseEntity의 메시지 바디에 담아서 전달을 할 수 있도록 했다. 해당 링크를 통해서 결제 요청을 할 수 있게 되는데, 결제 후에 카카오 서버 측에서 Redirect를 하는데, 위에서 작성한 "approval_url"로 Redirect를 하게 되는데, 즉, 본인이 Controller에 RequestMapping으로 설정해놓은 URL 주소인 "http://localhost:8080/payment/approval"로 Redirect를 해주게 되고, 이 때, 중요한 PG_Token을 받을 수 있게 된다.

결제 요청

위의 결제 준비를 통해서 Redirect로 결제 요청 관련 로직을 수행해야 하는데, 여기서 한 번 막히게 되었다. approval_url을 통해서 redirect가 되긴 했는데, 넘어오는 데이터가 pg_token 하나였고, 결제를 식별할 수 있는 방법이 없는 것 같았음(고유 번호인 tid가 같이 넘어오지 않음)

구글링 시작

대체 어떻게 문제를 해결할까 싶던 찰나에 본인과 같은 고민을 하던 개발자분이 계셨었는데 이미 질문글을 올렸고, 카카오페이 측에서도 답변을 달아줘서 해결할 수 있게 되었다.

위에서 제시하는 방법과 같이 "approval_url"을 작성할 때 기존의 "http://localhost:8080/payment/approval"에서 "http://localhost:8080/payment/approval?partner_order_id=xxx&partner_user_id=xxx"로 수정을 하게 되었다.

@GetMapping("/approval")
 public ResponseEntity<?> approvePayment(
   @RequestParam String pg_token,
   @RequestParam String partner_order_id,
   @RequestParam String partner_user_id
) {
  KakaoPaymentApprovalResponse info = kakaoPayment.getInfo(pg_token, partner_order_id, partner_user_id);
  if (info instanceof PaymentDto.KakaoPaymentApprovalSuccessResponse) {
   PaymentDto.KakaoPaymentApprovalSuccessResponse response =
     (PaymentDto.KakaoPaymentApprovalSuccessResponse) info;

   return ResponseEntity.ok(
     kakaoPaymentService.approvePayment(
       response
     ));
  } else {
   if (info instanceof PaymentDto.KakaoPaymentApprovalFailureResponse) {
    PaymentDto.KakaoPaymentApprovalFailureResponse response =
      (PaymentDto.KakaoPaymentApprovalFailureResponse) info;
    return ResponseEntity.ok(response);
   } else {
    return ResponseEntity.badRequest().build();
   }
  }
 }
public KakaoPaymentApprovalResponse getInfo(String pg_token, String partner_order_id, String partner_user_id) {

  GsonBuilder gsonBuilder = new GsonBuilder();
  gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
  Gson gson = gsonBuilder.create();

  Payment payment = paymentRepository.findByPartnerOrderId(partner_order_id)
    .orElseThrow(() -> new NotFoundException(MessageCode.PAYMENT_NOT_FOUND));

  MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

  params.add("cid", cid);
  params.add("tid", payment.getTid());
  params.add("partner_order_id", partner_order_id);
  params.add("partner_user_id", partner_user_id);
  params.add("pg_token", pg_token);

  HttpHeaders headers = new HttpHeaders();
  headers.set("Authorization", "KakaoAK " + admin_key);
  headers.set("Content-type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");

  HttpEntity<MultiValueMap<String, String>> httpRequest = new HttpEntity<>(params, headers);

  String requestUrl = "https://kapi.kakao.com/v1/payment/approve";
  ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, httpRequest, String.class);
  try {
   if (response.getStatusCode() == HttpStatus.OK) {
    return gson.fromJson(response.getBody(), PaymentDto.KakaoPaymentApprovalSuccessResponse.class);
   }
  } catch (RestClientException e) {
   e.printStackTrace();
  }
  return gson.fromJson(response.getBody(), PaymentDto.KakaoPaymentApprovalFailureResponse.class);
 }

첫 번째 결제를 준비할 때랑 다를 게 없어서 크게 어렵지 않다. pg_token과 저장하고 있던 정보들을 이용하면 된다. 그리고 항상 주의해야 할 것은 Token을 HttpHeader에 담아서 보낼 때 띄어쓰기("KakaoAK xxxxxxx") 꼭 해야 한다.

결제 완료

RestTemplate로 요청을 보냈을 때 받을 수 있는 응답이다. 결제 타입이 현금이냐 카드냐에 따라서 payment_method_type이 달라지고 결과도 조금씩 달라진다.

카드의 경우 Json데이터에 위의 응답 추가결제를 실패했을 때는 아래와 같은 데이터를 받게 되고, 미리 데이터를 받을 수 있는 객체를 만들어서 아래처럼 http status 체크는 꼭 해야 된다.

if (response.getStatusCode() == HttpStatus.OK) {
    return gson.fromJson(response.getBody(), PaymentDto.KakaoPaymentApprovalSuccessResponse.class);
   }

기본적인 요청 이렇게만 해도 충분할 것이다. 나중에 시간적 여유가 된다면, 결제 취소하는 요청도 구현해보겠다.


잘못된 정보는 지적해주시면 감사하겠습니다.

0개의 댓글