카카오페이를 구현하기 위해 여러 자료를 참고해서 코드를 작성해보았으나, 작동하지 않았다.
꽤 최신 블로그 글(약 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
연동하기 위해서는 먼저 카카오페이 개발자센터에 가입하고,
내 사이트 도메인을 등록해야 한다.
🔗 카카오페이 개발자센터 : https://developers.kakaopay.com/
이제 모든 준비는 끝났다. 코딩해서 구현해보자 !
* 카카오페이 개발자센터의 단건 결제 문서를 참고해서 구현했다.
🔗 URL : https://developers.kakaopay.com/docs/payment/online/single-payment
$.ajax()
메소드를 이용해 RequestBody로 보낼 데이터를JSON.stringify()
을 사용해서 JSON 형식으로 변환하여 전송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>
@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";
}
}
제공해주신 강사님 감사합니다 🙇🏻
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);
}
}
HashMap
에 저장HttpEntity
로 Map에 저장한 값들과 내 정보(getHeaders
)를 담아서 카카오페이 통신RestTemplate
을 통해 카카오의 REST API를 호출RestTemplate
의 postForEntity()
메소드를 사용해 응답으로 받은 결과를 ResponseEntity
의 getBody()
로 받아서 반환@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;
}
}
@Getter
@Setter
@ToString
public class ReadyResponse {
private String tid; // 결제 고유번호
private String next_redirect_pc_url; // 카카오톡으로 결제 요청 메시지(TMS)를 보내기 위한 사용자 정보 입력화면 Redirect URL (카카오 측 제공)
}
@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번 이미지와 같은 결과를 확인할 수 있다.
🔗 References
안녕하세요. 한 가지 궁금한게 있는데요.
컨트롤러에서
public @ResponseBody ReadyResponse payReady(@RequestBody OrderCreateForm orderCreateForm) {
OrderCreateForm은 어디서 만든 메소드인지 어떤 내용인지 알수있을까요?
잘 봤습니다
생초보인데 많은 도움이 됐습니다