본문 바로가기
Project/etc

[Spring Boot] 카카오페이 API 연동 - 팝업창 띄우기 및 결제승인까지

by ryuneng 2025. 1. 20.
반응형

카카오페이를 구현하기 위해 여러 자료를 참고해서 코드를 작성해보았으나, 작동하지 않았다.
꽤 최신 블로그 글(약 6개월 전 포스팅)을 참고해보기도 했지만, 여전히 오류만 날 뿐이었다.

도대체 뭐가 문제일까 찾아보다가 카카오페이 개발자센터의 공지를 자세히 보니,
무려 올해 1월3일자로 API 서비스에 변화가 생겼다고 한다.
🔗 카카오페이 공지 URL : https://developers.kakaopay.com/forum/t/api/281


능숙한 개발자라면 이 정도 변화에는 금방 돌파구를 찾겠지만,
초보 개발자인 나에게는 쉽지만은 않은 문제였다. 🤔
나같은 개린이를 위해, 또 시간이 지나면 헷갈릴 미래의 나를 위해 정리해본다.



❓ 바뀐 점

바뀐 점은 크게 3가지가 있었다.

1. Admin key에서 Secret key로 바뀌었다.
- 별거 아닐 수 있지만, 참고자료와 카카오페이 개발자센터의 명칭이 달라서 초반에 혼란스러웠음

2. 요청 url 형식이 바뀌었다.
ex) https://kapi.kakao.com/v1/payment/ready
-> https://open-api.kakaopay.com/online/v1/payment/ready

3. 지원하는 Map 종류가 달라졌다.
- LinkedMultiValueMap -> HashMap




연동하기 위해서는 먼저 카카오페이 개발자센터에 가입하고,
내 사이트 도메인을 등록
해야 한다.

✔️ 개발자센터 가입 및 등록

1. 카카오페이 개발자센터 가입

  • Kakao 아이디로 쉽게 가입 가능 (사업자 없어도 됨)

🔗 카카오페이 개발자센터 : https://developers.kakaopay.com/


2. 애플리케이션 등록

  • 내가 지정할 애플리케이션 이름을 입력해서 등록하면 된다.

3. 애플리케이션 플랫폼 등록

  • 연동할 사이트 도메인을 입력한다. 나는 배포 전이라 localhost로 작성했다.

4. Secret key(dev) 발급

  • 애플리케이션 > 기본정보 > 발급정보 > Secret key(dev) 발급
    • KakaoPayService 구현 시, 요청 헤더에 해당 키가 필요하다.



이제 모든 준비는 끝났다. 코딩해서 구현해보자 !

* 카카오페이 개발자센터의 단건 결제 문서를 참고해서 구현했다.
🔗 URL : https://developers.kakaopay.com/docs/payment/online/single-payment

📌 코드 작성

1. orderform.html

  • jQuery의 $.ajax() 메소드를 이용해 RequestBody로 보낼 데이터
    JSON.stringify()을 사용해서 JSON 형식으로 변환하여 전송
  • $.ajax()
    • url : Ajax 요청을 처리하는 서버측 URL
    • data : 서버로 보내는 데이터
    • contetntType : 서버로 보내는 데이터의 컨텐츠 타입
    • success : Ajax 요청이 성공했을 때 실행할 함수
      (여기서는 카카오페이 준비가 됐을 때 열릴 페이지,
      카카오페이 측의 Response Body Payload 이름이 next_redirect_pc_url)
<script type="text/javascript">
    // 카카오페이 결제 팝업창 연결
    $(function() {
        $("#btn-pay-ready").click(function(e) {
            // 아래 데이터 외에도 필요한 데이터를 원하는 대로 담고, Controller에서 @RequestBody로 받으면 됨
            let data = {
                name: '상품명',    // 카카오페이에 보낼 대표 상품명
                totalPrice: 20000 // 총 결제금액
            };
          
            $.ajax({
                type: 'POST',
                url: '/order/pay/ready',
                data: JSON.stringify(data),
                contentType: 'application/json',
                success: function(response) {
                    location.href = response.next_redirect_pc_url;
                }
            });
        });
    });
</script>


2. OrderController.java

@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/order")
public class OrderController {

    private final KakaoPayService kakaoPayService;
    
    @PostMapping("/pay/ready")
    public @ResponseBody ReadyResponse payReady(@RequestBody OrderCreateForm orderCreateForm) {
        
        String name = orderCreateForm.getName();
        int totalPrice = orderCreateForm.getTotalPrice();
        
        log.info("주문 상품 이름: " + name);
        log.info("주문 금액: " + totalPrice);

        // 카카오 결제 준비하기
        ReadyResponse readyResponse = kakaoPayService.payReady(name, totalPrice);
        // 세션에 결제 고유번호(tid) 저장
        SessionUtils.addAttribute("tid", readyResponse.getTid());
        log.info("결제 고유번호: " + readyResponse.getTid());

        return readyResponse;
    }

    @GetMapping("/pay/completed")
    public String payCompleted(@RequestParam("pg_token") String pgToken) {
    
        String tid = SessionUtils.getStringAttributeValue("tid");
        log.info("결제승인 요청을 인증하는 토큰: " + pgToken);
        log.info("결제 고유번호: " + tid);

        // 카카오 결제 요청하기
        ApproveResponse approveResponse = kakaoPayService.payApprove(tid, pgToken);

        return "redirect:/order/completed";
    }
}

  • SessionUtils
    - 카카오페이의 tid를 결제준비에서 결제승인으로 넘겨주기 위해 Session에 저장할 때 사용할 Util Class
    제공해주신 강사님 감사합니다 🙇🏻
public class SessionUtils {

    public static void addAttribute(String name, Object value) {
        Objects.requireNonNull(RequestContextHolder.getRequestAttributes()).setAttribute(name, value, RequestAttributes.SCOPE_SESSION);
    }

    public static String getStringAttributeValue(String name) {
        return String.valueOf(getAttribute(name));
    }

    public static Object getAttribute(String name) {
        return Objects.requireNonNull(RequestContextHolder.getRequestAttributes()).getAttribute(name, RequestAttributes.SCOPE_SESSION);
    }
}


3. KakaoPayService.java

  • 카카오페이에 전송할 값들을 HashMap에 저장
    (카카오페이 측에서 요청하는 Request Body Payload 중 Required 항목 필수 입력)
  • HttpEntity로 Map에 저장한 값들과 내 정보(getHeaders)를 담아서 카카오페이 통신
  • RestTemplate을 통해 카카오의 REST API를 호출
  • RestTemplatepostForEntity() 메소드를 사용해 응답으로 받은 결과를 ResponseEntitygetBody()로 받아서 반환
  • 최종적으로 Controller에서 그 반환받은 ReadyResponse를 HTML(클라이언트)에게 전송
@Slf4j
@Service
public class KakaoPayService {

    // 카카오페이 결제창 연결
    public ReadyResponse payReady(String name, int totalPrice) {
    
        Map<String, String> parameters = new HashMap<>();
        parameters.put("cid", "TC0ONETIME");                                    // 가맹점 코드(테스트용)
        parameters.put("partner_order_id", "1234567890");                       // 주문번호
        parameters.put("partner_user_id", "roommake");                          // 회원 아이디
        parameters.put("item_name", name);                                      // 상품명
        parameters.put("quantity", "1");                                        // 상품 수량
        parameters.put("total_amount", String.valueOf(totalPrice));             // 상품 총액
        parameters.put("tax_free_amount", "0");                                 // 상품 비과세 금액
        parameters.put("approval_url", "http://localhost/order/pay/completed"); // 결제 성공 시 URL
        parameters.put("cancel_url", "http://localhost/order/pay/cancel");      // 결제 취소 시 URL
        parameters.put("fail_url", "http://localhost/order/pay/fail");          // 결제 실패 시 URL

        // HttpEntity : HTTP 요청 또는 응답에 해당하는 Http Header와 Http Body를 포함하는 클래스
        HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());

        // RestTemplate
        // : Rest 방식 API를 호출할 수 있는 Spring 내장 클래스
        //   REST API 호출 이후 응답을 받을 때까지 기다리는 동기 방식 (json, xml 응답)
        RestTemplate template = new RestTemplate();
        String url = "https://open-api.kakaopay.com/online/v1/payment/ready";
        // RestTemplate의 postForEntity : POST 요청을 보내고 ResponseEntity로 결과를 반환받는 메소드
        ResponseEntity<ReadyResponse> responseEntity = template.postForEntity(url, requestEntity, ReadyResponse.class);
        log.info("결제준비 응답객체: " + responseEntity.getBody());

        return responseEntity.getBody();
    }

    // 카카오페이 결제 승인
    // 사용자가 결제 수단을 선택하고 비밀번호를 입력해 결제 인증을 완료한 뒤,
    // 최종적으로 결제 완료 처리를 하는 단계
    public ApproveResponse payApprove(String tid, String pgToken) {
        Map<String, String> parameters = new HashMap<>();
        parameters.put("cid", "TC0ONETIME");              // 가맹점 코드(테스트용)
        parameters.put("tid", tid);                       // 결제 고유번호
        parameters.put("partner_order_id", "1234567890"); // 주문번호
        parameters.put("partner_user_id", "roommake");    // 회원 아이디
        parameters.put("pg_token", pgToken);              // 결제승인 요청을 인증하는 토큰

        HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());

        RestTemplate template = new RestTemplate();
        String url = "https://open-api.kakaopay.com/online/v1/payment/approve";
        ApproveResponse approveResponse = template.postForObject(url, requestEntity, ApproveResponse.class);
        log.info("결제승인 응답객체: " + approveResponse);

        return approveResponse;
    }
    
    // 카카오페이 측에 요청 시 헤더부에 필요한 값
    private HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "카카오페이 개발자센터에서 발급받은 Secret key(dev) 입력");
        headers.set("Content-type", "application/json");

        return headers;
    }
}


4. ReadyResponse.java (DTO)

@Getter
@Setter
@ToString
public class ReadyResponse {

    private String tid;                  // 결제 고유번호
    private String next_redirect_pc_url; // 카카오톡으로 결제 요청 메시지(TMS)를 보내기 위한 사용자 정보 입력화면 Redirect URL (카카오 측 제공)
}


5. AproveResponse.java (DTO)

@Getter
@Setter
@ToString
public class ApproveResponse {

    private String aid;                 // 요청 고유 번호
    private String tid;                 // 결제 고유 번호
    private String cid;                 // 가맹점 코드
    private String partner_order_id;    // 가맹점 주문번호
    private String partner_user_id;     // 가맹점 회원 id
    private String payment_method_type; // 결제 수단, CARD 또는 MONEY 중 하나
    private String item_name;           // 상품 이름
    private String item_code;           // 상품 코드
    private int quantity;               // 상품 수량
    private String created_at;          // 결제 준비 요청 시각
    private String approved_at;         // 결제 승인 시각
    private String payload;             // 결제 승인 요청에 대해 저장한 값, 요청 시 전달된 내용
}



위 과정을 모두 끝낸 후,
카카오페이 결제가 연결된 버튼을 클릭하면 1번 이미지와 같은 페이지가 열리고,
휴대폰으로 QR 스캔하고 결제를 진행하면 2번 이미지와 같은 결과를 확인할 수 있다.

💡 실행 이미지

1. PC 카카오페이 결제준비 페이지

2. 휴대폰 QR 스캔 후 결제 진행


끝 !




🔗 References

 


< 해당 글은 velog에서 이전하며 옮겨온 글로, 가독성이 좋지 않을 수 있는 점 양해 부탁드립니다. >

🔗 velog 버전 보기 : https://velog.io/@ryuneng2/카카오페이-API-연동-팝업창띄우기-결제승인-구현