0. 왜 카카오페이 api를 사용하게 되었나?
현재 진행하고 있는 프로젝트에서 상품을 주문한 후 결제하는 기능이 필요했다. 실제로 결제가 되는 과정을 거치면서, 사용자들이 편하게 결제할 수 있는 방법에는 뭐가 있을까 생각하다 api를 가져다 쓰면 좋겠다는 생각이 들었고, 주위에서 사용빈도가 높은 카카오페이 api를 사용하면 사용자들이 편하게 결제를 진행할 수 있겠다는 생각을 하게 되었다. 그래서 카카오페이 api를 프로젝트에 적용해보고자 하였다.
아래는 카카오페이 api를 스프링 및 스프링부트에서 사용하기 위한 방법이다.
흐름 정리
프론트엔드가 백엔드에게 결제 정보를 담은 후 api1를 요청한다. 그러면 백엔드는 api1에 대한 처리로 카카오페이 서버에게 결제를 하고 싶다는 요청을 보내고 응답으로 결제를 할 수 있는 화면으로 갈 수 있는 url을 받는다. 그럼 백엔드는 이 내용을 프론트엔드에게 전달한다.
프론트엔드는 api1의 요청으로 받은 응답을 토대로 결제할 수 있는 url로 이동시킨다. 이 화면에서 사용자는 결제를 완료한다. 정상적으로 완료되면 백엔드가 설정해놓은 결제 완료 후 가게 되는 api2로 자동으로 넘어가게 된다. 그러면 백엔드는 api2에 대한 처리로 카카오페이 서버에게 이런 내용으로 결제를 진행했다는 요청을 보내고 응답으로 결제 승인이 되었다는 내용을 받는다. 그럼 백엔드는 이 내용을 프론트엔드에게 전달한다.
1. Kakao Developers
이 사이트에서 회원가입을 한 후 애플리케이션을 하나 만든다. 프론트가 리액트네이티브이긴 한데, 일단 웹으로만 등록을 해줬다.
그리고 내 애플리케이션-내가 만든 앱 클릭-앱키에 가 보면 Admin키가 있다. 그걸 복사한다.
2. application.properties
## kakaopay
kakao.pay.admin-key=위에서 복사한 adminkey
kakao.pay.ready-url=https://kapi.kakao.com/v1/payment/ready
kakao.pay.approve-url=https://kapi.kakao.com/v1/payment/approve
kakao.pay.cid=TC0ONETIME -> 실제 결제가 아니라 테스트용으로 결제를 하기 위한 설정
3. Controller
1) 결제를 요청하기 위한 컨트롤러(readyPayment)
프론트엔드에서 카카오페이로 결제하기 버튼을 눌러서 결제 과정을 진행할 수 있게 할 api를 위한 컨트롤러를 만든다. 예를 들어 readyPayment라고 하자.
프론트엔드는 이 api에 요청을 보낼 때 Order 테이블에 저장할 정보를 담은 내용을 dto에 담아서 보내야 한다. 결제 진행되고 있는 메뉴, 결제를 진행하고 있는 사용자, 상품 이름, 상품 가격 등.
2) 결제 성공 후 가게 되는 컨트롤러(afterPayRequest)
readyPayment의 결과로 프론트엔드는 KakaoPayReadyResponseDto를 응답받게 된다. 여기에는 결제를 할 수 있는 곳으로 갈 수 있는 url이 담겨 있고 이 url에서 카카오페이를 이용해 결제를 진행한다.
결제 진행을 정상적으로 완료한 후 가야 될 url을 스프링 서버가 명시해야 하는데 그 url이 이 컨트롤러이다.
카카오페이 서버는 결제가 정상적으로 완료된 후 가야 될 url에 ptToken을 파라미터로 담아서 보낸다. 그런데 우리는 이 컨트롤러에서 실행할 서비스단에서 해당 결제의 주문 id도 필요해서 해당 id를 payReady 서비스에서 결제가 정상적으로 완료된 후 가야 될 url에 파라미터로 함께 넘겼다.
@PostMapping("/kakaopay/ready")
public KakaoPayReadyResponseDto readyPayment(@RequestBody KakaoPayRequestDto kakaoPayRequestDto) {
return kakaoPayService.payReady(kakaoPayRequestDto);
}
@GetMapping("/kakaopay/success")
public KakaoPayApproveResponseDto afterPayRequest(@RequestParam("pg_token") String pgToken, @RequestParam("orderId") String orderId){
return kakaoPayService.payApprove(pgToken, orderId);
}
4. Service
1) 결제를 요청하기 위한 서비스(payReady)
프론트엔드가 readyPayment에 요청을 보내면 컨트롤러는 이 처리를 결제를 요청하기 위한 서비스로 넘긴다.
먼저 payReady는 컨트롤러부터 넘겨받은 dto 정보를 Order 테이블에 저장한다. 이때 중요한 것! 무작정 넘겨받은 order를 저장하다가는 A 고객이 메뉴1에 결제 요청해서 결제를 막 진행하고 있는 와중에 B 고객이 메뉴1에 결제를 요청하면 order가 2개가 생기게 된다. 따라서 이를 막기 위해 parReady에 왔을 때 해당 메뉴 id가 order 테이블에 이미 있는지, 즉 해당 메뉴가 주문이 진행중인지를 확인해야 한다. 나는 일단
if(orderRepository.findByMenuEntityId(kakaoPayRequestDto.getMenuId()).isPresent()){
return null;
}
이런 형태로 확인해 주었는데.... 이렇게 데이터베이스 ACID를 적절히 관리하기 위한 효과적인 방법이 무엇인지 더 알아봐야 할 것 같다.
그럼 이 서비스단에서는 스프링 서버가 카카오페이 서버에 결제를 진행하고 싶어요라는 요청을 보내게 되고, 그럼 카카오페이 서버는 결제를 진행할 수 있도록 스프링 서버에게 결제id, 결제 진행으로 넘어갈 수 있는 url등을 보내게 되고, 스프링 서버는 프론트엔드에게 이 내용을 전달한다.
payReady는 카카오페이 서버로부터 받은 tid를 위에서 생성한 Order테이블 행에 저장한다. 이는 나중에 결제 진행 과정을 거치고 tid를 사용할 일이 있어서 필요하다.
2) 결제 성공 후 가게 되는 서비스(payApprove)
afterPayRequest 컨트롤러에서 비즈니스 로직 처리를 위해 실행하는 서비스. 카카오페이 서버로 완료된 결제에 대한 내용을 담아 요청을 보내고 결제가 완료되었다는 승인에 대한 응답을 받는다. 그리고 이 응답을 컨트롤러에게 보내서 프론트엔드에게 전달한다.
public KakaoPayReadyResponseDto payReady(KakaoPayRequestDto kakaoPayRequestDto){
// orderEntity 생성
OrderEntity order = saveOrderEntity(kakaoPayRequestDto);
// 카카오페이 서버로 보내기 위한 준비
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();
parameters.add("cid", cid);
parameters.add("partner_order_id", String.valueOf(order.getId()));
parameters.add("partner_user_id", String.valueOf(kakaoPayRequestDto.getUserId()));
parameters.add("item_name", kakaoPayRequestDto.getName());
parameters.add("quantity", "1");
parameters.add("total_amount", String.valueOf(kakaoPayRequestDto.getPrice()));
parameters.add("tax_free_amount", "0");
parameters.add("approval_url", "http://localhost:8080/api/payment/kakaopay/success"+"?orderId="+String.valueOf(order.getId())); // 결제승인시 넘어갈 url
parameters.add("cancel_url", "http://localhost:8080/api/payment/kakaopay/cancel"); // 결제취소시 넘어갈 url
parameters.add("fail_url", "http://localhost:8080/api/payment/kakaopay/fail"); // 결제 실패시 넘어갈 url
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
// 카카오페이 서버로 요청 보내기
RestTemplate template = new RestTemplate();
KakaoPayReadyResponseDto readyResponse = template.postForObject(readyUrl, requestEntity, KakaoPayReadyResponseDto.class);
// tid 저장
saveTid(order.getId(), readyResponse);
return readyResponse;
}
public OrderEntity saveOrderEntity(KakaoPayRequestDto kakaoPayRequestDto){
OrderEntity order = OrderEntity.builder()
.status("T")
.menuEntity(menuRepository.findById(kakaoPayRequestDto.getMenuId()).get())
.userEntity(userRepository.findById(kakaoPayRequestDto.getUserId()).get())
.build();
return orderRepository.save(order);
}
public void saveTid(long orderId, KakaoPayReadyResponseDto readyResponse){
Optional<OrderEntity> order = orderRepository.findById(orderId);
if(order.isPresent()){
OrderEntity savedOrder = OrderEntity.builder()
.id(order.get().getId())
.tid(readyResponse.getTid())
.status(order.get().getStatus())
.menuEntity(order.get().getMenuEntity())
.userEntity(order.get().getUserEntity())
.build();
orderRepository.save(savedOrder);
}
}
public KakaoPayApproveResponseDto payApprove(String pgToken, String orderId){
// tid를 찾아와야 함
OrderEntity order = orderRepository.findById(Long.parseLong(orderId)).get();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("cid", cid);
parameters.add("tid", order.getTid());
parameters.add("partner_order_id", String.valueOf(order.getId()));
parameters.add("partner_user_id", String.valueOf(order.getUserEntity().getId()));
parameters.add("pg_token", pgToken);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, this.getHeaders());
RestTemplate restTemplate = new RestTemplate();
KakaoPayApproveResponseDto approveResponse = restTemplate.postForObject(
approveUrl,
requestEntity,
KakaoPayApproveResponseDto.class);
return approveResponse;
}
// header() 셋팅
private HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + admin);
headers.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
return headers;
}
5. Dto
1) 프론트엔드가 readyPayment에게 보내는 Dto
상품 가격, 상품 이름, 메뉴 id, 유저 id 등의 내용을 담고 있다.
public class KakaoPayRequestDto {
private long userId;
private long menuId;
private String name;
private int price;
}
2) 결제 요청 후 응답 받는 Dto(KakaoPayReadyResponseDto)
payReady에서 카카오 페이 서버로 결제 요청을 하면 카카오 페이 서버로부터 응답받는 내용을 담고 있는 dto.
public class KakaoPayReadyResponseDto {
private String tid;
private String next_redirect_mobile_url;
private String next_redirect_pc_url;
private String partner_order_id;
}
3) 결제 완료 후 응답 받는 Dto(KakaoPayApproceResponseDto)
payApprove에서 카카오페이 서버로 결제 승인 요청을 하면 카카오페이 서버로부터 응답받는 내용을 담고 이는 dto.
public class KakaoPayApproveResponseDto {
private String aid;
private String tid;
private String cid;
private String sid;
private String partner_order_id;
private String partner_user_id;
private String payment_method_type;
private String item_name;
private String item_code;
private int quantity;
private String created_at;
private String approved_at;
private String payload;
private KakaoPayHistoryResponseDto kakaoPayHistoryResponseDto;
}
'프로젝트 > 싹쓰리' 카테고리의 다른 글
[프로젝트] 스프링에서 구글 GeoCoding으로 주소로부터 위도 경도 얻기 (0) | 2023.08.24 |
---|---|
[프로젝트] 스프링에서 채팅 기능 구현해보기 (0) | 2023.07.16 |
[프로젝트] 도커에 대해서 (0) | 2023.06.06 |
[프로젝트] ServiceInterface와 ServiceImpl (0) | 2023.04.23 |
[프로젝트] 객체 생성 방법(생성자, getter setter, 빌더) (0) | 2023.04.16 |