Kakao Pay API를 이용한 결제 구현 (단건 결제)
- 카카오 페이 API는 Admin Key를 호출하므로 Git Hub 사용 시 보안에 주의해야 한다
- @ConfigurationProperties와 .gitignore를 이용
결제 프로세스
- 결제 준비와 결제 승인 프로세스
- 시퀀스 다이어그램
** 사용자(고객), 서버(사이트)
[결제 준비 프로세스]
1) 사용자가 사이트에 구매 신청
2) 서버에서 카카오페이로 결제 준비 요청
3) 카카오페이에서 서버로 결제 준비 응답 (결제를 위해 필요한 정보와 결제 페이지에 대한 URL 포함)
4) 서버에서 사용자를 카카오페이로부터 전송받은 결제 준비 응답의 결제 페이지 URL로 Redirect
5) 사용자가 Redirect된 URL로 카카오페이 결제 페이지를 연결하기 위한 요청 전송
[카카오페이 내 결제 과정] ** 카카오페이 내에서 진행되는 과정이므로 정확한 과정인지는 모르겠음
6) 카카오페이에서 사용자를 카카오 페이 결제 페이지로 연결
7) 사용자가 카카오페이로 결제 수단, 포인트 사용 등의 결제 정보를 전송
[결제 승인 프로세스]
8) 카카오페이에서 결제 성공/실패/취소 여부에 따라 서로 다른 주소로 연결
- Controller에서 해당 주소의 Mapping을 생성하여 JSP로 연결
9) 서버에서 카카오페이에 결제 승인 요청
10) 카카오페이에서 서버로 결제 승인 응답, 서버는 결제 정보를 DB에 등록
11) 서버에서 사용자를 결제 성공 Mapping으로 Redirect
결제 준비
기본 정보
** Authorization은 본인의 Admin Key, CID는 테스트 결제용 CID인 TC0ONETIME로 한다
결제 준비 요청에 포함되는 정보
String | cid | 가맹점 코드, 10자 |
String | partner_order_id | 가맹점 주문번호, 최대 100자 |
String | partner_user_id | 가맹점 회원 ID, 최대 100자 |
String | item_name | 상품명, 최대 100자 |
Integer | quantity | 상품 수량 |
Integer | total_amount | 상품 총액 |
Integer | tax_free_amount | 상품 비과세 금액 |
String | approval_url | 결제 성공시 redirect url |
String | cancel_url | 결제 취소시 redirect url |
String | fail_url | 결제 실패시 redirect url |
결제 준비 응답에 포함되는 정보
String | tid | 결제 고유 번호, 20자 |
String | next_redirect_app_url | 요청한 클라이언트(Client)가 모바일 앱일 경우 카카오톡 결제 페이지 Redirect URL |
String | next_redirect_mobile_url | 요청한 클라이언트가 모바일 웹일 경우 카카오톡 결제 페이지 Redirect URL |
String | next_redirect_pc_url | 요청한 클라이언트가 PC 웹일 경우 카카오톡으로 결제 요청 메시지(TMS)를 보내기 위한 사용자 정보 입력 화면 Redirect URL |
String | android_app_scheme | 카카오페이 결제 화면으로 이동하는 Android 앱 스킴(Scheme) |
String | ios_app_scheme | 카카오페이 결제 화면으로 이동하는 iOS 앱 스킴 |
String | created_at | 결제 준비 요청 시간 |
application.properties
- 보안을 위해 Admin Key와 cid(가맹점 코드)를 사용자 정의 속성으로 설정한 후 .gitignore에 추가(업로드 방지)
- 설정값을 직접적으로 사용하기 위해서는 해당 속성의 값을 바인딩할 VO를 만들어야 한다 (KakaoPayProperties)
KakaoPayProperties
- application.properties의 사용자 속성의 값을 바인딩하기 위한 VO (@ConfigurationProperties)
- application.properties의 key 값을 key 필드에 바인딩하여 REST API 요청의 header에 포함될 Admin Key로 사용
- application.properties의 cid 값을 cid 필드에 바인딩하여 REST API 요청의 body에 포함될 tid로 사용
@Data
@Component
@ConfigurationProperties(prefix = "custom.pay")
public class KakaoPayProperties {
private String key; // REST API 요청의 header에서 Authorization 값
private String cid; // REST API 요청의 body에서 cid 값
}
KakaoPayReadyRequestVO
- 카카오페이 결제 준비 요청을 위한 VO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KakaoPayReadyRequestVO {
private String partner_order_id; // 가맹점 주문번호
private String partner_user_id; // 가맹점 회원 id
private String item_name; // 상품명
private int total_amount; // 상품 총액
}
KakaoPayReadyResponseVO
- 카카오페이 결제 준비 요청에 대한 응답을 위한 VO
@Data
public class KakaoPayReadyResponseVO {
private String tid; // 결제 고유 번호, 20자
private String next_redirect_mobile_url; // 모바일 웹에 대한 카카오페이 결제 페이지 Redirect URL
private String next_redirect_pc_url; // PC 웹에 대한 카카오페이 결제 페이지 Redirect URL
private Date created_at; // 결제 준비 요청 시간
}
KakaoPayService
public interface KakaoPayService {
// 추상 메소드 - 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
KakaoPayReadyResponseVO ready(KakaoPayReadyRequestVO request) throws URISyntaxException;
}
KakaoPayServiceImpl
@Slf4j
@Service
public class KakaoPayServiceImpl implements KakaoPayService {
// REST 방식의 API를 호출하기 위한 RestTemplate의 인스턴스 생성
private RestTemplate template = new RestTemplate();
// 의존성 주입 - Admin Key와 cid를 반환하기 위함
@Autowired
private KakaoPayProperties kakaoPayProperties;
// 추상 메소드 오버라이딩 - 결제 준비 요청을 보낸 후 결제 준비 응답을 반환
@Override
public KakaoPayReadyResponseVO ready(KakaoPayReadyRequestVO request) throws URISyntaxException {
// 결제 준비 요청을 보낼 주소 설정
URI uri = new URI("https://kapi.kakao.com/v1/payment/ready");
// REST API 요청의 header 설정
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "KakaoAK " + kakaoPayProperties.getKey()); // Admin Key
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// REST API 요청의 body 설정
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("cid", kakaoPayProperties.getCid()); // 가맹점 코드
body.add("partner_order_id", request.getPartner_order_id()); // 가맹점 주문 번호
body.add("partner_user_id", request.getPartner_user_id()); // 가맹점 회원 ID
body.add("item_name", request.getItem_name()); // 상품명
body.add("quantity", "1"); // 상품 수량
body.add("total_amount", String.valueOf(request.getTotal_amount())); // 상품 총액
body.add("tax_free_amount", "0"); // 상품 비과세 금액
body.add("approval_url", "http://localhost:8888/pay/result/success"); // 결제 성공시 Redirect 주소
body.add("cancel_url", "http://localhost:8888/pay/result/cancel"); // 결제 취소시 Redirect 주소
body.add("fail_url", "http://localhost:8888/pay/result/fail"); // 결제 실패시 Redirect 주소
// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
// 요청 전송 후 KakaoPayReadyResponseVO 형태로 응답 반환
KakaoPayReadyResponseVO response = template.postForObject(uri, entity, KakaoPayReadyResponseVO.class);
// 응답 반환
return response;
}
}
HttpEntity
- HTTP 요청 및 응답을 위한 header와 body를 포함하는 Entity(데이터)
- header와 body 각각의 속성명과 그 값을 MultiValueMap에 저장
MultiValueMap<K, V>
- 하나의 key에 여러 개의 value를 저장할 수 있는 Map (key의 중복을 허용)
RestTemplate
- REST 방식의 API를 호출할 수 있는 Spring의 내장 클래스
<T> T | postForObject(URI uri, Object request, Class<T> responseType ) | POST 방식으로 요청을 보내고 해당 responseType의 응답 반환 |
PayController
@Controller
public class PayController {
// 의존성 주입
@Autowired
private SqlSession sqlSession;
// 의존성 주입
@Autowired
private KakaoPayService kakaoPayService;
// 사이트 결제 Mapping
@GetMapping("/pay1")
public String pay1() {
// 사이트 결제 페이지(pay1.jsp)로 연결
return "pay1";
}
// 카카오페이 결제 준비 Mapping
@PostMapping("/pay1")
public String pay1(@ModelAttribute KakaoPayReadyRequestVO request, HttpSession session) throws URISyntaxException {
// 결제 준비 요청을 위한 VO에 정보 설정
request.setPartner_order_id(UUID.randomUUID().toString()); // 가맹점 주문 번호 설정
String memberId = (String)session.getAttribute("loginId"); // HttpSession에서 회원 아이디 반환
request.setPartner_user_id(memberId); // 반환한 회원 아이디를 가맹점 회원 ID로 설정
// 카카오페이로 결제 준비 요청 전송 후 응답 반환
KakaoPayReadyResponseVO response = kakaoPayService.ready(request);
// 결제 승인을 위한 값을 미리 HttpSession에 저장
session.setAttribute("tid", response.getTid());
session.setAttribute("partner_order_id", request.getPartner_order_id());
session.setAttribute("partner_user_id", request.getPartner_user_id());
// 사용자를 결제 준비 응답에 포함된 PC URL로 강제 이동(redirect)
return "redirect:" + response.getNext_redirect_pc_url();
}
}
pay1.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<h1>직접 입력해서 결제</h1>
<form action = "pay1" method = "post">
상품명 : <input type = "text" name = "item_name" required>
구매금액 : <input type = "number" name = "total_amount" required>
<button type = "submit">구매하기</button>
</form>
결제 승인
기본 정보
** Authorization은 본인의 Admin Key, CID는 테스트 결제용 CID인 TC0ONETIME로 한다
결제 승인 요청에 포함되는 정보
String | cid | 가맹점 코드, 10자 |
String | tid | 결제 고유 번호, 결제 준비 API 응답에 포함 |
String | partner_order_id | 가맹점 주문번호, 결제 준비 API 요청과 일치해야함 |
String | partner_user_id | 가맹점 회원 ID, 결제 준비 API 요청과 일치해야함 |
Integer | pg_token | 결제 승인 요청을 인증하는 토큰 사용자 결제 수단 선택 완료시, approval_url로 redirection 해줄 때 pg_token을 queryString으로 전달 |
결제 승인 응답에 포함되는 정보
String | aid | 요청 고유 번호 |
String | tid | 결제 고유 번호 |
String | cid | 가맹점 코드 |
String | sid | 정기 결제용 ID, 정기결제 CID로 단건결제 요청 시 발급 |
String | partner_order_id | 가맹점 주문번호 |
String | partner_user_id | 가맹점 회원 ID |
String | payment_method_type | 결제 수단, CARD 또는 MONEY 중 하나 |
Amount | amount | 결제 금액 정보 |
CardInfo | card_info | 결제 상세 정보, 결제 수단이 카드인 경우만 포함 |
String | item_name | 상품 이름, 최대 100자 |
String | item_code | 상품 코드, 최대 100자 |
Integer | quantity | 상품 수량 |
Date | created_at | 결제 준비 요청 시각 |
Date | approved_at | 결제 승인 시각 |
String | payload | 결제 승인 요청에 대해 저장한 값, 요청시 전달한 내용 |
amount
Integer | total | 전체 결제 금액 |
Integer | tax_free | 비과세 금액 |
Integer | vat | 부가세 금액 |
Integer | point | 사용한 포인트 금액 |
Integer | discount | 할인 금액 |
Integer | green_deposit | 컵 보증금 |
card_info
String | purchase_corp | 매입 카드사 한글명 |
String | purchase_corp_code | 매입 카드사 코드 |
String | issuer_corp | 카드 발급사 한글명 |
String | issuer_corp_code | 카드 발급사 코드 |
String | kakaopay_purchase_corp | 카카오페이 매입사명 |
String | kakaopay_purchase_corp_code | 카카오페이 매입사 코드 |
String | kakaopay_issuer_corp | 카카오페이 발급사명 |
String | kakaopay_issuer_corp_code | 카카오페이 발급사 코드 |
String | bin | 카드 BIN |
String | card_type | 카드 타입 |
String | install_month | 할부 개월수 |
String | approved_id | 카드사 승인 번호 |
String | card_mid | 카드사 가맹점 번호 |
String | interest_free_install | 무이자 할부 여부(Y/N) |
String | card_item_code | 카드 상품 코드 |
KakaoPayProperties
- application.properties의 사용자 속성의 값을 바인딩하기 위한 VO (@ConfigurationProperties)
- application.properties의 key 값을 key 필드에 바인딩하여 REST 요청의 header에 포함될 Admin Key로 사용
- application.properties의 cid 값을 cid 필드에 바인딩하여 REST 요청의 body에 포함될 tid로 사용
@Data
@Component
@ConfigurationProperties(prefix = "custom.pay")
public class KakaoPayProperties {
private String key; // REST API 요청의 header에서 Authorization 값
private String cid; // REST API 요청의 body에서 cid 값
}
KakaoPayApproveRequestVO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KakaoPayApproveRequestVO {
private String tid; // 결제 고유 번호
private String partner_order_id; // 가맹점 주문 번호
private String partner_user_id; // 가맹점 회원 ID
private String pg_token; // 결제 승인 요청 인증토큰
}
KakaoPayApproveResponseVO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KakaoPayApproveResponseVO {
private String aid; // 요청 고유 번호
private String tid; // 결제 고유 번호
private String cid; // 가맹점 코드
private String sid; // 정기 결제용 ID
private String partner_order_id; // 가맹점 주문번호
private String partner_user_id; // 가맹점 회원번호
private String payment_method_type; // 결제 수단(CARD/MONEY)
private AmountVO amount; // 결제 금액 정보
private CardInfoVO card_info; // 결제 카드 정보
private String item_name; // 상품 이름
private String item_code; // 상품 코드
private int quantity; // 상품 수량
private Date created_at; // 결제 준비 요청 시각
private Date approved_at; // 결제 승인 시각
private String payload; // 결제 요청 시 전달된 요청 데이터
}
KakaoPayService
public interface KakaoPayService {
// 추상 메소드 - 결제 승인 요청을 보낸 후 결제 승인 응답을 반환
KakaoPayApproveResponseVO approve(KakaoPayApproveRequestVO request) throws URISyntaxException;
}
KakaoPayServiceImpl
- 결제 준비에서처럼 Authorization과 CID의 값은 application.properties에서 바인딩한 사용자 정의 속성의 값을 사용
@Slf4j
@Service
public class KakaoPayServiceImpl implements KakaoPayService {
// REST 방식의 API를 호출하기 위한 RestTemplate의 인스턴스 생성
private RestTemplate template = new RestTemplate();
// 의존성 주입 - Admin Key와 cid를 반환하기 위함
@Autowired
private KakaoPayProperties kakaoPayProperties;
// 추상 메소드 오버라이딩 - 결제 조회 요청을 보낸 후 결제 조회 응답을 반환
@Override
public KakaoPayOrderResponseVO order(KakaoPayOrderRequestVO request) throws URISyntaxException {
// 결제 조회 요청을 보낼 주소 설정
URI uri = new URI("https://kapi.kakao.com/v1/payment/order");
// REST API 요청의 header 설정
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "KakaoAK " + kakaoPayProperties.getKey());
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// REST API 요청의 body 설정
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("cid", kakaoPayProperties.getCid());//가맹점번호(테스트용)
body.add("tid", request.getTid());
// REST API 요청을 위한 HttpEntity 생성 - header와 body 결합
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
// 요청 전송 후 KakaoPayOrderResponseVO 형태로 응답 반환
KakaoPayOrderResponseVO response = template.postForObject(uri, entity, KakaoPayOrderResponseVO.class);
// 응답 반환
return response;
}
}
PayController
@Controller
public class PayController {
// 의존성 주입
@Autowired
private SqlSession sqlSession;
// 의존성 주입
@Autowired
private KakaoPayService kakaoPayService;
// 카카오페이 결제 승인 Mapping
@GetMapping("/pay/result/success")
public String paySuccess(HttpSession session, @RequestParam String pg_token) throws URISyntaxException {
// 결제 승인을 위해 HttpSession에 저장했던 값을 반환
String tid = (String)session.getAttribute("tid");
String partner_order_id = (String)session.getAttribute("partner_order_id");
String partner_user_id = (String)session.getAttribute("partner_user_id");
List<ProductDto> list = (List<ProductDto>)session.getAttribute("list");
List<PurchaseItemVO> data = (List<PurchaseItemVO>)session.getAttribute("data");
// 반환이 끝난 값을 HttpSession에서 삭제
session.removeAttribute("tid");
session.removeAttribute("partner_order_id");
session.removeAttribute("partner_user_id");
session.removeAttribute("list");
session.removeAttribute("data");
// 결제 승인 요청을 위한 VO에 정보 설정
KakaoPayApproveRequestVO request = KakaoPayApproveRequestVO.builder()
.tid(tid)
.partner_order_id(partner_order_id)
.partner_user_id(partner_user_id)
.pg_token(pg_token)
.build();
// 카카오페이로 결제 승인 요청 전송 후 응답 반환
KakaoPayApproveResponseVO response = kakaoPayService.approve(request);
// 결제 승인 완료 Mapping으로 강제 이동(redirect)
return "redirect:/pay/result/success_view";
}
// 카카오페이 결제 승인 완료 Mapping
@GetMapping("/pay/result/success_view")
public String successView() {
// 최종 결제 성공 페이지(succes_view.jsp)로 연결
return "success_view";
}
}
'국비교육 > 국비교육' 카테고리의 다른 글
day79 - 1118 (0) | 2022.11.18 |
---|---|
day78 - 1117 (0) | 2022.11.18 |
day76 - 1115 (0) | 2022.11.16 |
day75 - 1114 (0) | 2022.11.14 |
day73 - 1110 (0) | 2022.11.12 |