[SpringBoot] 카카오페이 데모
카카오페이 데모 단건결제를 참고해 구현된 샘플 앱입니다.
1. 카카오페이 결제 준비
- 결제준비 요청
- 결제 버튼을 클릭해 post 요청을 보낼 때 필요한 url (https://kapi.kakao.com/v1/payment/ready)
- HttpHeader 정의 시 필요한 App key와 Content-type
- tid (결제 고유 번호)
- post 요청 후 요청하는 redirect url: next_redirect_pc_url (카카오페이 QR 코드 페이지)
결제준비 시 Service 코드
parameters 객체 안에 들어가는 파라미터들은 카카오페이 결제 시 필요한 필수 요청 파라미터이다. 컨트롤러에서 정의하기에 너무 코드양이 길고 지저분해져서 service 측에서 따로 정의해 주었다.
즉,
- HttpHeader 정의
- 카카오에서 부여받은 App Key와 content-type을 정의해 준다
- 결제에 필요한 필수 요청 파라미터들(결제정보 및 리다이렉트 url)을 parameters 객체에 담는다
- 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. 카카오페이 결제 승인
결제 승인 시 service 코드
결제 요청과 마찬가지로 필요한 파라미터를 multiValueMap에 담아 승인 요청 url로 보낸다.
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. 카카오페이 결제 순서 (총정리)
- 주문페이지에서 결제 준비 요청을 서버로 보낸다. (GET/POST 방식 모두 가능하고, 주문 관련 모든 정보를 요청핸들러 메소드로 보내는 것이 가능하다)
- 컨트롤러의 [요청핸들러] 메소드에서 결제 준비 요청을 카카로오 보낸다. (가맹점 정보, 구매정보, 결제 성공/실패/취소 시 이동할 URL)
- 결제요청승인 아이디(tid), QR코드가 표시되는 웹 페이지를 요청하는 URL을 응답으로 보낸다.
- 응답으로 받은 URL로 이동한다.
- 스마트폰으로 QR 코드를 찍어서 결제를 진행하고, 결제를 완료한다.
- 결제가 완료되면 카카오페이는 결제준비 요청할 때 보낸 URL을 요청한다. (GET 방식만 가능하고, 요청핸들러 메소드로 보내는 정보도 pg_token만 가능하다)
- 5번에서 요청한 URL에 대응되는 컨트롤러의 [요청핸들러] 메소드가 실행된다.
요청핸들러 메소드가 두 번 실행된다. 크게 결제 준비, 결제 승인 단계로 나뉘는 것이다.
결제 관련 요청을 처리하는 업무로직의 처리
- 위의 결제 순서에서 1번과 6번에서 각각 사용자정의 요청핸들러 메소드가 실행된다.
- 1번에서 주문정보를 저장하고, 6번까지 완료되면 6번에서 해당 주문의 주문 상태를 결제 완료 상태로 설정한다.