[SpringBootJPA] 카카오페이 결제API (단건결제)
market 개인 프로젝프에 결제 시스템으로 카카오페이 api를 사용하기로 했다.
카카오페이 API는 문서가 너무 잘 정리되어 있어서 구현하는데에 있어서 큰 어려움은 없었지만, 그래도 개발 기록을 해보려고 한다 !
카카오페이 API 문서는 여기에서 확인할 수 있다. (원래는 kakao developers에 있었지만 최근 kakaopay developers로 따로 분리 되었음)
준비 과정
먼저 로그인을 한 뒤 카카오페이 애플리케이션을 만들어야 한다.
그리고 플랫폼에서 Web을 설정하였다.
그리고 기본 정보에서 Secret Key(dev)를 발급받는다. (나는 개인프로젝트로 로컬에서만 사용하므로 테스트용으로서 발급받았음)
그러면 이제 준비가 끝났다 ! 바로 해보면 된다.
단건 결제
위에서 잠깐 말했듯이 결제하려면 가맹점 코드가 필요하지만, 나처럼 개인 프로젝트로 로컬에서만 한다면 카카오페이에서 또 테스트해볼 수 있도록 주어진 코드가 있다! 위의 코드를 사용하면 된다.
나는 Secret key와 CID를 application-pay.yml을 따로 만들어서 저장하였다. (개인정보로 github에 올라가지 않도록 하기 위해!)
그리고 KakaoPayProperties로 component 등록하였다.
[application-pay.yml]
kakaopay:
secretKey: Secret key(dev)
cid: TC0ONETIME
[KakaoPayProperteis]
@Component
@ConfigurationProperties(prefix = "kakaopay")
public class KakaoPayProperties {
private String secretKey;
private String cid;
// Getters and Setters
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getCid() {
return cid;
}
public void setCid(String cid) {
this.cid = cid;
}
}
결제 준비 (ready)
Request
Request Syntax를 보면 header에 Authorization과 Content-type을 넣어서 보내야 한다.
[service]
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class KakaoPayService {
private final KakaoPayProperties payProperties;
private RestTemplate restTemplate = new RestTemplate();
private HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
String auth = "SECRET_KEY " + payProperties.getSecretKey();
headers.set("Authorization", auth);
headers.set("Content-Type", "application/json");
return headers;
}
* 이게 과거에는 Kapi ? 뭐 이렇게 앞에 들어갔던거 같은데 이제 SECRET_KEY 로 넣어야 한다!!!
그리고 이제 위의 예시에 맞춰서 결제 요청을 보내야 한다.
[service]
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class KakaoPayService {
private final KakaoPayProperties payProperties;
private RestTemplate restTemplate = new RestTemplate();
private KakaoReadyResponse kakaoReady;
private HttpHeaders getHeaders() {
. . .
}
/**
* 결제 완료 요청
*/
public KakaoReadyResponse kakaoPayReady() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("cid", payProperties.getCid());
parameters.put("partner_order_id", "ORDER_ID"); // 실제 주문 번호로 교체
parameters.put("partner_user_id", "USER_ID"); // 실제 사용자 ID로 교체
parameters.put("item_name", "ITEM_NAME"); // 실제 상품명으로 교체
parameters.put("quantity", "1"); // 수량, 숫자는 문자열로 전달
parameters.put("total_amount", "2200"); // 총 금액, 숫자는 문자열로 전달
parameters.put("vat_amount", "200"); // 부가세, 숫자는 문자열로 전달
parameters.put("tax_free_amount", "0"); // 비과세 금액, 숫자는 문자열로 전달
parameters.put("approval_url", "Web에서 등록한 URL/success");
parameters.put("fail_url", "Web에서 등록한 URL/fail");
parameters.put("cancel_url", "Web에서 등록한 URL/cancel");
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
// 외부에 보낼 url
RestTemplate restTemplate = new RestTemplate();
kakaoReady = restTemplate.postForObject(
"https://open-api.kakaopay.com/online/v1/payment/ready",
requestEntity,
KakaoReadyResponse.class);
return kakaoReady;
}
* 여기서 tip이 있다면, 내가 약간 고생했던 부분이 있었다 !
MultiValueMap을 Map 대신 사용했었는데, 여기서 MultiValueMap을 사용한다면 카카오페이 api가 원하지 않는 값인 '['']' 와 같은 대괄호가 들어가기 때문에 원하는 결과가 나오지 않는다. 성공적인 결과를 얻기 위해 대괄호를 없애주던가, 아니면 Map을 사용해야 한다.
(나도 알고 싶지 않았다..)
+ 잘한거 같은데 원하지 않는 결과가 나온다면 잘하지 못하고 있는 것! 어딘가가 빵꾸가 난 것이다. 그것을 찾기위해 꼭 sout 이나, log를 뽑아내볼 것!!! 다짐다짐!!!
[controller]
@RestController
@RequestMapping("/payment")
@RequiredArgsConstructor
public class KakaoPayController {
private final KakaoPayService kakaoPayService;
/**
* 결제요청
*/
@PostMapping("/ready")
public KakaoReadyResponse readyToKakaoPay() {
return kakaoPayService.kakaoPayReady();
}
Response
[KakaoReadyResponse]
@Data
public class KakaoReadyResponse {
private String tid;
private String next_redirect_app_url;
private String next_redirect_mobile_url;
private String next_redirect_pc_url;
private String android_app_scheme;
private String ios_app_scheme;
private String created_at;
}
swagger-ui를 통해 요청에 성공한 값이 나오는 것을 확인했다.
그리고 저기에 결과로 뜬 주소를 들어가면 페이 결제창이 나온다.
결제 요청 및 결제 승인 (approve)
Request
[service]
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class KakaoPayService {
private final KakaoPayProperties payProperties;
private RestTemplate restTemplate = new RestTemplate();
private KakaoReadyResponse kakaoReady;
private HttpHeaders getHeaders() {
. . .
}
public KakaoReadyResponse kakaoPayReady() {
. . .
}
/**
* 결제 완료 승인
*/
public KakaoApproveResponse approveResponse (String pgToken){
// 카카오 요청
Map<String, String> parameters = new HashMap<>();
parameters.put("cid", payProperties.getCid());
parameters.put("tid", kakaoReady.getTid());
parameters.put("partner_order_id", "ORDER_ID");
parameters.put("partner_user_id", "USER_ID");
parameters.put("pg_token", pgToken);
// 파라미터, 헤더
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
System.out.println();
System.out.println();
System.out.println(requestEntity);
System.out.println();
System.out.println();
// 외부에 보낼 url
RestTemplate restTemplate = new RestTemplate();
KakaoApproveResponse approveResponse = restTemplate.postForObject(
"https://open-api.kakaopay.com/online/v1/payment/approve",
requestEntity,
KakaoApproveResponse.class);
System.out.println();
System.out.println();
System.out.println();
System.out.println(approveResponse);
System.out.println();
System.out.println();
System.out.println();
return approveResponse;
}
[controller]
@RestController
@RequestMapping("/payment")
@RequiredArgsConstructor
public class KakaoPayController {
private final KakaoPayService kakaoPayService;
/**
* 결제요청
*/
@PostMapping("/ready")
public KakaoReadyResponse readyToKakaoPay() {
. . .
}
/**
* 결제성공
*/
@PostMapping ("/success")
public ResponseEntity<KakaoApproveResponse> afterPayRequest(@RequestParam("pg_token") String pgToken) {
KakaoApproveResponse kakaoApprove = kakaoPayService.approveResponse(pgToken);
return new ResponseEntity<>(kakaoApprove, HttpStatus.OK);
}
/**
* 결제 진행 중 취소
*/
@GetMapping("/cancel")
public void cancel() {
throw new BusinessLogicException(ExceptionCode.PAY_CANCEL);
}
/**
* 결제실패
*/
@GetMapping("/fail")
public void fail() {
throw new BusinessLogicException(ExceptionCode.PAY_FAILED);
}
* BusinessLogicException은 따로 만들어줬다.
결제를 준비하고, 준비 후 결제 요청이 맞다면 pg_token을 받게 되고, pg_token을 넘기면 결제가 완료된다.
Response
[KakaoApproveResponse]
/**
* 결제 승인 요청 시 사용
*/
@Getter
@Setter
@ToString
public class KakaoApproveResponse {
private String aid; // 요청 고유 번호
private String tid; // 결제 고유 번호
private String cid; // 가맹점 코드
private String sid; // 정기결제용 ID
private String partner_order_id; // 가맹점 주문 번호
private String partner_user_id; // 가맹점 회원 id
private String payment_method_type; // 결제 수단
private Amount amount; // 결제 금액 정보
private String item_name; // 상품명
private String item_code; // 상품 코드
private int quantity; // 상품 수량
private String created_at; // 결제 요청 시간
private String approved_at; // 결제 승인 시간
private String payload; // 결제 승인 요청에 대해 저장 값, 요청 시 전달 내용
}
[Amount]
/**
* 결제 금액 정보
*/
@Getter
@Setter
@ToString
public class Amount {
private int total; // 총 결제 금액
private int tax_free; // 비과세 금액
private int tax; // 부가세 금액
private int point; // 사용한 포인트
private int discount; // 할인금액
private int green_deposit; // 컵 보증금
}
이렇게 결제가 성공적으로 이루어지면 이렇게 카톡으로 온다!
성공적인 결제 api 구현 완료 !!!