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

류넹·2024년 4월 13일
2

Project

목록 보기
1/7

카카오페이를 구현하기 위해 여러 자료를 참고해서 코드를 작성해보았으나, 작동하지 않았다.
꽤 최신 블로그 글(약 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

profile
학습용 커스터마이징 간단 개발자 사전

12개의 댓글

comment-user-thumbnail
2024년 9월 21일

잘 봤습니다
생초보인데 많은 도움이 됐습니다

1개의 답글
comment-user-thumbnail
2024년 11월 27일

안녕하세요. 한 가지 궁금한게 있는데요.
컨트롤러에서
public @ResponseBody ReadyResponse payReady(@RequestBody OrderCreateForm orderCreateForm) {

OrderCreateForm은 어디서 만든 메소드인지 어떤 내용인지 알수있을까요?

1개의 답글
comment-user-thumbnail
2024년 12월 3일

안녕하세요 잘봤습니다!
궁금한 점이 있는데, approval_url을 백엔드 포트로 해서 api주소로 설정하신건가요?
이렇게 설정할 경우 승인은 나는데, 기존 Pc페이지(프론트는 localhost 포트3000번 입니다) 프론트 페이지에서
그냥 검정 화면에 approve response json만 띄워져 있는 상태입니다... 혹시 승인 완료 이후에는 프론트 부분은 어떻게 설정하셨나요??

1개의 답글
comment-user-thumbnail
2025년 1월 6일

안녕하세요 카카오페이 결제 기능 구현 관련해 질문 남깁니다!
카카오페이 결제 정보 테이블은 따로 없는 건가요?

1개의 답글

관련 채용 정보