본문 바로가기

국비교육/국비교육

day77 - 1116

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