카카오페이 데모 단건결제를 참고해 구현된 샘플 앱입니다.

1. 카카오페이 결제 준비

image

  • 결제준비 요청
    1. 결제 버튼을 클릭해 post 요청을 보낼 때 필요한 url (https://kapi.kakao.com/v1/payment/ready)
    2. HttpHeader 정의 시 필요한 App key와 Content-type
    3. tid (결제 고유 번호)
    4. post 요청 후 요청하는 redirect url: next_redirect_pc_url (카카오페이 QR 코드 페이지)

결제준비 시 Service 코드

parameters 객체 안에 들어가는 파라미터들은 카카오페이 결제 시 필요한 필수 요청 파라미터이다. 컨트롤러에서 정의하기에 너무 코드양이 길고 지저분해져서 service 측에서 따로 정의해 주었다.

즉,

  1. HttpHeader 정의
    • 카카오에서 부여받은 App Key와 content-type을 정의해 준다
  2. 결제에 필요한 필수 요청 파라미터들(결제정보 및 리다이렉트 url)을 parameters 객체에 담는다
  3. HttpEntity 객체로 정의한 header와 parameters를 매핑한다
    @Slf4j
    @Service
    public class KakaoPayService {

        public ReadyResponse payReady(Book book, int quantity, int totalPrice) {

            // 요청 파라미터 
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();
            parameters.add("cid", "TC0ONETIME"); // 가맹점 코드, 테스트 결제는 TC0ONETIME를 사용한다
            parameters.add("partner_order_id", "1234567890"); // 가맹점 주문번호
            parameters.add("partner_user_id", "북스토어"); // 가맹점 회원 아이디
            parameters.add("item_name", book.getTitle()); // 주문 상품명
            parameters.add("item_code", String.valueOf(book.getNo())); // 주문 상품코드
            parameters.add("quantity", String.valueOf(quantity)); // 주문 수량
            parameters.add("total_amount", String.valueOf(totalPrice)); // 총 주문 금액
            parameters.add("tax_free_amount", "0"); // 상품 비과세 금액
            parameters.add("approval_url", "http://localhost/order/pay/completed"); // 결제 성공시 리다이렉트 URL
            parameters.add("cancel_url", "http://localhost/order/pay/cancel"); // 결제 취소시 리다이렉트 URL
            parameters.add("fail_url", "http://localhost/order/pay/fail"); // 결제 실패시 리다이렉트 URL
            
            // 3. 요청정보를 포함하는 객체 생성
            HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
            
            // 카카오에 요청을 보내고, 응답을 받는 RestTemplate 객체 생성
            RestTemplate template = new RestTemplate();
            String url = "https://kapi.kakao.com/v1/payment/ready";

            // 요청 URL, 요청정보를 포함하는 객체
            // ReadyResponse는 tid와 next_redirect_pc_url을 담고 있다
            ReadyResponse readyResponse = template.postForObject(url, requestEntity, ReadyResponse.class);
            log.info("결제준비 응답객체: " + readyResponse);
            
            return readyResponse;
        }
        
        // 요청 헤더
        private HttpHeaders getHeaders() {
            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization", "KakaoAK {APP_ADMIN_KEY}"); // 카카오 측에서 부여받은 APP Key
            headers.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
            
            return headers;
        }
    }

카카오페이 결제 시 서버는 가맹점정보, 주문상품 정보, 리다이렉트 url을 카카오 측에 요청한다. 그리고 카카오는 tid(결제고유 번호)와 redirect url(카카오페이 QR 코드 결제 화면)을 반환한다.

RestTemplate 객체의 postForObject 메소드 정의는 다음과 같다. Create a new resource by POSTing the given object to the URI template,and returns the representation found in the response. 즉 post 요청을 할 때 주어진 requestEntity 객체를 지정된 url로 보내고, ReadyResponse 객체로 결과를 반환받는다.

결제준비 시 Controller 코드

컨트롤러 코드 다음으로 나올 스크립트 코드를 살펴 보면 카카오페이는 tid와 next_redirect_pc_url을 서버에게 응답으로 보낸다. 서버는 그 url을 클라이언트에게 응답으로 보낸다. (QR코드 페이지가 뜨는 순간임) 컨트롤러 코드를 살펴보면 tid를 session 객체에 담은 것@SessionAttributes({"tid"})을 확인할 수 있다. 이는 응답으로 받은 결제고유 번호를 따로 저장해 두기 위해서이다.

즉, tid는 @GetMapping("/pay/ready") 요청 핸들러 메소드에서 get 할 수 있지만 tid를 사용하는 것은 @GetMapping("/pay/completed")이기 때문이다.

    @Controller
    @RequestMapping("/order")
    @SessionAttributes({"tid"})
    public class OrderController {

        @GetMapping("/pay/ready")
        public @ResponseBody ReadyResponse payReady(@RequestParam("no") int bookNo, int quantity, int totalPrice, Model model) {
            
            Book book = bookService.getBook(bookNo);
            
            // 카카오에 결제준비 요청을 보내고, 결제준비 응답을 받는다.
            // 결제 준비 응답에는 결제고유번호(tid), QR 코드 페이지 URL이 포함되어 있다.
            ReadyResponse readyResponse = kakaoPayService.payReady(book, quantity, totalPrice);
            
            // 결제고유 번호(tid)를 세션에 저장시킨다.
            model.addAttribute("tid", readyResponse.getTid());
            
            return readyResponse;
        }
    }

결제준비 시 script 코드

    <script>
    $(function() {
        // 결제하기 버튼 클릭 시 실행할 요청핸들러 함수 등록
        $("#btn-pay-ready").click(function(e) {
            
            $.ajax({
                type: 'get', 
                url: '/order/pay/ready',
                data: {
                    // 주문상품번호, 주문수량, 사용쿠폰 번호, 사용 포인트, 총 주문 수량, 총 결제금액 등
                    no: $("#book-title").attr("data-book-no"),
                    quantity: $("#form-order :input[name=quantity]").val(),
                    totalPrice: $("#total-price").attr("data-total-price"),
                },
                success:function(response) {
                    // response = {tid: T1234567890123456789, next_redirect_pc_url: https://mockup-pg-web.kakao.com/v1/xxxxxxxxxx/info}
                    // 웹브라우저 주소창의 URL을 QR 코드 페이지로 이동시킨다.
                    location.href= response.next_redirect_pc_url;
                }
            });
        });
    })
    <script>

2. 카카오페이 결제 승인

image

결제 승인 시 service 코드

결제 요청과 마찬가지로 필요한 파라미터를 multiValueMap에 담아 승인 요청 url로 보낸다.

image

	public ApproveResponse payApprove(String tid, String pgToken) {
		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();
		parameters.add("cid", "TC0ONETIME");
		parameters.add("tid", tid);
		parameters.add("partner_order_id", "1234567890");
		parameters.add("partner_user_id", "북스토어");
		parameters.add("pg_token", pgToken);
		
		HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
		
		RestTemplate template = new RestTemplate();
		String url = "https://kapi.kakao.com/v1/payment/approve";
		ApproveResponse approveResponse = template.postForObject(url, requestEntity, ApproveResponse.class);
		log.info("결제승인 응답객체: " + approveResponse);
		
		return approveResponse;
	}

결제 승인 시 controller 코드

approval_url을 http://localhost/order/pay/completed로 정의해 뒀었기 때문에, 결제 요청이 승인되면 아래 요청핸들러 메소드가 실행된다. 카카오는 결제 요청이 승인되면 pg_token(결제승인 요청을 인증하는 토큰)을 서버에게 전달한다. 서버는 payApprove 메소드를 실행해 payApprove에서 생성한 requestEntity를 승인 요청하는 url(/v1/payment/approve)로 보내고, 주문 정보 저장 후 주문완료 페이지를 요청하는 url을 재요청한다.


    @Controller
    @RequestMapping("/order")
    @SessionAttributes({"tid"})
    public class OrderController {

        @GetMapping("/pay/completed")
	    public String payCompleted(@RequestParam("pg_token") String pgToken, @ModelAttribute("tid") String tid, Authentication authentication, Model model) {
            User user = (User) authentication.getPrincipal();
            
            log.info("결제승인 요청을 인증하는 토큰: " + pgToken);
            log.info("결제 고유 번호: " + tid);
            
            // 카카오 결제 요청하기
            ApproveResponse approveResponse = kakaoPayService.payApprove(tid, pgToken);	
            
            // 주문정보 저장에 필요한 정보 및 객체 생성
            int userNo = user.getNo();
            int totalPrice = approveResponse.getAmount().getTotal();
            int bookNo = Integer.parseInt(approveResponse.getItem_code());	
            int quantity = approveResponse.getQuantity();
            
            OrderItem orderItem = new OrderItem();
            orderItem.setBookNo(bookNo);
            orderItem.setQuantity(quantity);
            
            // OrderService의 saveOrder를 실행해서 주문정보를 저장시킨다
            orderService.saveOrder(userNo, tid, totalPrice, orderItem);
            
            // 주문완료 페이지를 요청하는 URL을 재요청으로 보낸다
            return "redirect:/order/completed?id=" + tid;
        }
    }

3. 카카오페이 결제 순서 (총정리)

  1. 주문페이지에서 결제 준비 요청을 서버로 보낸다. (GET/POST 방식 모두 가능하고, 주문 관련 모든 정보를 요청핸들러 메소드로 보내는 것이 가능하다)
  2. 컨트롤러의 [요청핸들러] 메소드에서 결제 준비 요청을 카카로오 보낸다. (가맹점 정보, 구매정보, 결제 성공/실패/취소 시 이동할 URL)
  3. 결제요청승인 아이디(tid), QR코드가 표시되는 웹 페이지를 요청하는 URL을 응답으로 보낸다.
  4. 응답으로 받은 URL로 이동한다.
  5. 스마트폰으로 QR 코드를 찍어서 결제를 진행하고, 결제를 완료한다.
  6. 결제가 완료되면 카카오페이는 결제준비 요청할 때 보낸 URL을 요청한다. (GET 방식만 가능하고, 요청핸들러 메소드로 보내는 정보도 pg_token만 가능하다)
  7. 5번에서 요청한 URL에 대응되는 컨트롤러의 [요청핸들러] 메소드가 실행된다.

요청핸들러 메소드가 두 번 실행된다. 크게 결제 준비, 결제 승인 단계로 나뉘는 것이다.

결제 관련 요청을 처리하는 업무로직의 처리

  • 위의 결제 순서에서 1번과 6번에서 각각 사용자정의 요청핸들러 메소드가 실행된다.
  • 1번에서 주문정보를 저장하고, 6번까지 완료되면 6번에서 해당 주문의 주문 상태를 결제 완료 상태로 설정한다.