카카오페이 결제 시스템 적용

    새로 진행하는 프로젝트에서는 공동구매라는 기능이 있어, 카카오페이 및 토스페이먼츠 결제 시스템을 적용하기로 했다.

     

    여기서 나는 카카오페이 결제 시스템을 담당하게 되었으며 이 글은 그에 따라 구현하는 과정을 적고자 한다.

     

    애플리케이션 등록

    https://developers.kakaopay.com/applications

     

    카카오페이 | 개발자센터

    새로운 기회와 가치를 함께 만들어봐요

    developers.kakaopay.com

    페이지를 통해서 애플리케이션을 등록하고

    ClientId, Client Secret, Secret Key, Secret Key(dev) 을 발급받았다.

    이  프로젝트는 배포는 진행하지만 실제 사용자를 위한 결제 시스템을 적용할 계획은 아니었기 때문에 개발자 테스트모드로 진행을 한다.

     

    ※ 개발자 테스트 모드로 진행을 하게 되면, 실제 금액을 사용하지 않고도 결제 시스템을 구축해볼 수 있다.

     

    또한, 아무것도 기반되는 지식이 없기 때문에

    https://developers.kakaopay.com/docs/payment/online/single-payment

     

    카카오페이 | 개발자센터

    새로운 기회와 가치를 함께 만들어봐요

    developers.kakaopay.com

    문서에 따라서 진행을 해보려 했다.

     

    결제 시스템 구축을 시작하며..

    처음 직접 코드를 작성하고 통신을 진행하기 전까지는 많은 혼동이 있었다.

    회의를 진행해도, 실제로 프론트와 백엔드 사이에서 어떤 데이터가 오가야하는지 파악하기가 힘들었으며, 어떠한 방식으로 결제가 진행되는지를 블로그 글들만으로 파악하는 것이 힘들었다.

    따라서, 처음 시작은 백엔드 서버에서 thymeleaf를 통해서 우선적으로 진행하였다.

    다행히도 카카오 개발자 문서에 샘플 코드가 존재해 훨씬 더 편하게 진행할 수 있었다.

    https://developers.kakaopay.com/docs/payment/online/reference#sample-source

     

    카카오페이 | 개발자센터

    새로운 기회와 가치를 함께 만들어봐요

    developers.kakaopay.com

     

    처음 샘플 코드를 보게 된 이유는 다음과 같았다.

    이와 같은 방식으로 코드를 작성했었는데 자꾸 cid가 invalid하다는 오류 메시지를 받게 되었다.

    분명히 로그를 찍어봐도 정상적으로 들어가는데 무엇이 문제인지 몰랐고 카카오 샘플 코드를 참고하여 다음과 같이 변경했다

    이렇게 작성하고 나니 정상적으로 응답값이 돌아오게 되었다.

     

    원인은 다음과 같았다.

    MultiValueMap을 사용할 때는 application/x-www-form-urlencoded 방식으로 처리되는데, 개발자 문서를 확인해 보면 Application.JSON 타입을 요구한다고 되어 있다. 따라서 원하는 형식의 body 값으로 들어오지 않아 오류가 발생한 것 같다.

    또한, 현재는 업데이트 된 것 같은데 이전에 구현한 블로그들을 참고하면 Secret_key, Content-Type, 요청 url이 다르기 때문에 항상 구현함에 있어서는 관련 문서를 참고하는 것이 중요할 듯 하다.

     

    나는 테스트 모드로 진행할 예정이기에 Secret key(dev)와, 가맹점 코드는 TC0ONETIME을 사용했다.

     

    카카오페이 결제 시스템 구조

    카카오페이 결제 시스템 구조는 다음과 같다.

    1. 가맹점 코드, Secret_Key, 필요 데이터 등을 통해 결제 준비 요청을 카카오페이 서버에 보내게 된다.
    2. 정상적인 값들을 확인하게 되면 카카오페이 서버는 tid(결제 고유 번호), 결제 요청 url(안드로이드, ios, pc), created_at(결제 준비 요청 시간) 등을 보내준다. (현재는 pc 웹 기준으로만)
    3. 결제 요청 url로 redirect를 하면 우리가 흔히 아는 카카오페이 결제 qr코드 화면이 뜬다.
    4. 이제 사용자가 결제를 진행한다.
    5. 결제 승인,취소,실패 여부에 따라 위에서 작성한 approvalUrl, cancelUrl, failUrl 로 자동으로 redirect 된다.
    6. 서버는 이에 맞춰 컨트롤러를 생성하고 처리한다.

    개발자 샘플 문서를 확인해보면 친절하게 타임리프 페이지까지 제공하고 있기 때문에 백엔드 서버만으로 테스트를 진행하려면 이를 참고하면 도움이 될 것이다.

     

    프론트엔드와의 연결

    실질적으로 우리는 프론트엔드와 통신을 통해 결제를 해야하기 때문에 애플리케이션 플랫폼에서 추가 등록을 해주었다.

    우선은 배포서버와 진행을 하기 이전에 로컬호스트를 통해 진행을 하고 추후 합치려 한다.

    프론트엔드와의 통신에서 생각한 점은 다음과 같다.

    1. 준비 단계에서 백엔드 자체 통신을 할때는 임의의 데이터를 넣어서 GetMapping으로 진행했지만, 실제로는 상품 데이터를 받아야 하기 때문에 컨트롤러를 PostMapping으로 변경
    2. DB에 결제 번호에 따른 tid(결제 고유 번호) 저장
    3. 결제 페이지에서 결제 완료가 될 경우 프론트에서 다시 요청을 보내는 것이 아닌, 백엔드 승인 컨트롤러로 redirect 처리
    4. 결국 마지막에 끝나는 페이지는 백엔드의 ~~/approve 페이지일 텐데, 이 창에서 프론트로 어떻게 데이터를 보내줄 것인가? -> postMessage를 사용

    하는 방식으로 생각을 하였다. 간단하게 그림을 그려보면 다음과 같다.

     

    그렇다면 다시 요청에 맞게 코드를 변경해보자

    먼저, 프론트에서 백엔드로 결제 준비 요청을 받게 된다. 이 때, 배송지와 상품 관련 데이터를 Post 요청을 이용해서 받는다

    들어온 데이터에 따라서 새로운 주문 엔티티를 생성하고

    요청을 보낸다. 이 때 redirect되는 url들에 orderNum을 파라미터로 전달해주었다.

    이 방식을 통해서 redirect 되었을 때, 어떤 주문에 관련된 것인지 파악할 수 있을 것이라고 생각했다.

    이제, 프론트로 next_redirect_pc_url을 전달해주었다면, 프론트에서는 새 창을 이용해 next_redirect_pc_url을 띄워준다.

     

    그 이후 결제가 완료되면, 우리가 설정해둔 대로 /pay/kakao/approve?orderNum=~~&pgToken=~~ 같은 페이지로 redirect되게 된다. 이 때, host를 백엔드 서버로 해두었기 때문에

    이 컨트롤러로 들어오게 될 것이다.

    이제 우리는 카카오페이 서버에 결제 승인이 되었다고 요청을 보내야 한다.

    이렇게, 요청을 보낸 후에 주문의 상태를 결제가 완료되었다는 것으로 보낸다.

    현재 상황을 정리해보면

    • 프론트는 상품과 배송지 정보를 통해 결제 준비 요청을 보내고 결제를 요청하는 페이지를 띄워주었다.
    • 사용자는 새로 띄워진 페이지에서 결제를 진행한다.
    • 백엔드는 결제 준비 요청 시에 새로운 주문을 만들고, 사용자가 결제를 완료하면 결제 승인 요청을 카카오페이 서버에 보낸다.

    즉, 현재 상황에서 프론트엔드는 백엔드의 주문에 대한 정보를 알고 있는 것이 없다.(주문번호, 주문 상태 등)

    따라서, 백엔드 -> 프론트엔드로 주문과 관련해 데이터를 보내주어야 한다.(여기서는 주문 번호를 전달한다)

     

    여기서, 아마 Spring Security 기능을 이용해서 Oauth 로그인 기능을 구현할 때와 비슷하게, 새로운 창의 끝이 백엔드 서버이다.

    (지금 포스팅에서는, ~~/pay/kakao/approve?orderNum=~~&pgToken=~~ 의 창에서 멈추게 된다.) 

    이 때 데이터를 프론트엔드로 보내려고 하지만, redirect를 이용해서는 바디라던지 헤더를 통해서 데이터를 보낼 수 없게 된다.

    이런 식으로, 요청과 응답의 쌍이 맞지 않기 때문에 데이터를 보낼 수 없다. 이를 해결할 수 있는 방법은 2가지가 있다.

    1. url 쿼리 사용: 백엔드에서 프론트로 redirect를 할 때에, ${front}/~~?orderNum=123123 처럼 보내면, 프론트에서 이를 파악해서 페이지를 띄워준다.
    2. PostMessage 사용: 새로운 창에서, 부모 창에 message event를 보내는 방법이다. 이를 통해 새로운 창에서 부모 창으로 데이터를 안전하게 보낼 수 있다.

    이전에 작성한 oauth 로그인 글에서는 회원과 관련된 정보가 들어있고, 메인 페이지로 이동을 시켜야하기 때문에 Spring Security의 기능을 이용하지 않고, 요청-응답 쌍을 맞춰 직접 github나 google에 요청을 보내는 식으로 구현을 하였다.

     

    이번에는 주문번호만 보낼 예정이기 때문에 url 쿼리를 사용해도 되지만, 기본적으로 프론트 페이지에서 href를 통해서 next_redirect_pc_url로 이동을 하는 것이 아닌 새 창을 여는 것이기 때문에, redirect와 url 쿼리를 사용하게 된다면 결제를 할 때마다 계속해서 새로운 창이 생길 것이라 생각했다.

    따라서, PostMessage를 이용해서 부모 창으로 데이터를 보내고, 프론트는 이를 받은 후 새로운 창을 닫는 방식으로 사용자가 1개의 페이지에서 사용을 한다는 느낌을 받을 수 있도록 하였다.

    PostMessage 사용

    이런식으로 html 템플릿을 하나 만든 후 type, orderNum을 프론트로 전달하였다.

    type은 각 프로젝트에서 정하기에 따라 다르고 우리는 결제성공, 결제실패, 결제취소를 프론트로 전달하고 프론트에서는 이 상태에 따라서 분기점을 나누어 처리를 다르게 하기로 하였다.

    PostMessage를 보낼 때에 content-type은 text/html;charset=UTF-8로 지정하였다

    댓글