메인 프로젝트를 하면서 카카오 API를 이용하여 카카오 페이를 구현해보았다.
해당 부분을 기억하기 위해 이 글을 작성하였다.
이 포스팅은 프로젝트 과정에서 흐름을 기억하기 위해 작성한 것으로, 기본 흐름의 틀정도라고 생각하면 좋을 듯 하다.
해당 포스팅의 코드는 아래의 깃헙 주소에서도 확인 가능.
✨ 들어가기 전에, 메인 프로젝트에서 사용한 카카오 페이의 흐름 ✨
: 아래의 흐름을 바탕으로 프로젝트의 코드에 약간의 변형을 주어 구현할 예정
📚자료 참조 : https://developers.kakao.com/docs/latest/ko/kakaopay/common
📎 구현 전에 준비
- ⭐️ Kakao Developers에서 애플리케이션 설정하기 ⭐️
- 위의 사이트로 들어가 애플리케이션을 생성해준다.
- 요약 정보에서 앱 키를 확인할 수 있다.
- 앱 설정 👉🏻 플랫폼 👉🏻Web 사이트 도메인 등록
1️⃣ application.yml 생성
- application.properties 파일을 yml 파일로 변경
- 어드민 키 환경변수 설정
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
kakao:
adminKey: ${KAKAO_ADMIN_KEY}
2️⃣ 필요한 응답 객체 3가지 생성
- 결제 내역에 사용할 Amount
- 결제 요청에 사용할 ReadyResponse
- 결제 승인에 사용할 ApproveResponse
@Getter
@Setter
@ToString
public class Amount {
private int total; /* 전체 금액 */
private int tax_free; /* 비과세 금액 */
private int vat; /* 부과세 금액 */
private int point; /* 사용한 포인트 */
private int discount; /* 할인 금액 */
}
/* 결제 요청할 때 사용 */
@Getter
@Setter
@ToString
public class ReadyResponse {
private String tid; /* 결제 고유 번호 */
private String next_redirect_pc_url; /* 카카오 결제 창이 나오는 url */
private String partner_order_id; /* 가맹점 주문 번호 (최대 100자) */
}
/* 결제 승인 시 담아 보냄 */
@Getter
@Setter
@ToString
public class ApproveResponse {
private String aid; /* 요청 고유 번호 */
private String tid; /* 결제 고유 번호 */
private String cid; /* 가맹점 코드 */
private String sid; /* 정기결제용 ID, 정기결제 CID 로 단건결제 요청 시 발급 */
private String partner_order_id; /* 가맹점 주문 번호 (최대 100자) */
private String partner_user_id; /* 가맹점 회원 아이디 (최대 100자) */
private String payment_method_type; /* 결제 수단 (카드 혹은 현금) */
private String item_name; /* 상품 이름 (최대 100자) */
private String item_code; /* 상품 코드 (최대 100자) */
private int quantity; /* 주문 수량 */
private String created_at; /* 결제 준비 요청 시간 */
private String approved_at; /* 결제 승인 시간 */
private String payload; /* 결제 승인 요청에 대해 저장한 값, 요청 시 전달된 내용 */
private Amount amount; /* 결제 금액 정보 */
}
3️⃣ KakaoPayService 구현
- 결제 준비 요청
- 결제 승인 요청
- 결제 실패 혹은 취소
⭐️ 헤더에 어드민 키를 넣을 때, 앞에 "KakaoAk " 필수 ⭐️
: 해당 부분 넣지 않고 에러를 만난 경험이 있음.
❓주문 DB에 tid를 저장하는 이유 : 그러지 않으면 이후에 tid를 불러올 수 가 없음.
❓게시판의 정보를 수정하는 이유 : (나의 프로젝트의 경우임) 주문 등록 시 빠지는 재고를 취소가 되면 되돌리기 위해.
❓주문 상태의 변경 이유 : (나의 프로젝트의 경우임) 결제 완료가 된 소비자만 리뷰 작성 가능.
❓url에 주문 번호를 추가하는 이유 : 주문 식별을 위해, 그리고 여러 결제가 동시에 이루어질 때 꼬이지 않기 위해.
@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoPayService {
private final OrdRepository ordRepository;
private final BoardRepository boardRepository;
@Value("${spring.kakao.adminKey}")
private String adminKey;
/* 결제 준비 */
public ReadyResponse payReady(long ordId) {
Ord ord = findVerifiedOrd(ordId);
/* 카카오가 요구한 결제 요청 */
MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
param.add("cid", "TC0ONETIME"); /* 테스트 용 가맹점 코드 */
param.add("partner_order_id", String.valueOf(ord.getOrdId())); /* 가맹점 주문번호 -> String 형 */
param.add("partner_user_id", "17farm");
param.add("item_name", ord.getProduct().getBoard().getTitle());
param.add("quantity", String.valueOf(ord.getQuantity())); /* 개수는 상수형 */
param.add("total_amount", String.valueOf(ord.getTotalPrice())); /* 상품 총액 */
param.add("tax_free_amount", "0"); /* 상품 비과세 금액 */
param.add("approval_url", "http://localhost:8080/order/pay/completed" + ord.getOrdId()); /* 결제 승인 url */
param.add("cancel_url", "http://localhost:8080/order/pay/cancel" + ord.getOrdId()); /* 결제 취소 url */
param.add("fail_url", "http://localhost:8080/order/pay/fail" + ord.getOrdId()); /* 결제 실패 url */
HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(param, this.getHeaders());
RestTemplate template = new RestTemplate();
String url = "https://kapi.kakao.com/v1/payment/ready";
ReadyResponse readyResponse = template.postForObject(url, requestEntity, ReadyResponse.class);
log.info("결제 준비 응답 객체 확인 : " + readyResponse);
ord.setTid(readyResponse.getTid());
ordRepository.save(ord);
return readyResponse;
}
/* 승인 요청 메서드 */
public OrdResponseDto payApprove(Long ordId, String pgToken) {
Ord findOrd = findVerifiedOrd(ordId);
/* 요청 값 */
MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
param.add("cid", "TC0ONETIME");
param.add("tid", findOrd.getTid()); /* 결제 고유번호, 결제 준비 API 응답에 포함 */
param.add("partner_order_id", String.valueOf(findOrd.getOrdId())); /* 가맹점 주문번호, 결제 준비 API 요청과 일치해야 함 */
param.add("partner_user_id", "17farm"); /* 가맹점 회원 id, 결제 준비 API 요청과 일치해야 함 */
param.add("pg_token", pgToken); /* 결제승인 요청을 인증하는 토큰 */
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(param, this.getHeaders());
/* 외부 url 통신 */
RestTemplate template = new RestTemplate();
String url = "https://kapi.kakao.com/v1/payment/approve";
ApproveResponse approveResponse = template.postForObject(url, requestEntity, ApproveResponse.class);
log.info("결제 승인 응답 객체 확인: " + approveResponse);
/* 주문 상태 변경 */
findOrd.setStatus(Ord.OrdStatus.PAY_COMPLETE);
ordRepository.save(findOrd);
return OrdResponseDto.builder()
.ordId(ordId)
.build();
}
/* 결제 취소 혹은 삭제 */
public void cancelOrFailPayment(Long ordId) {
/* 결제 취소 or 결제 실패 -> 재고 수정 */
Ord findOrd = findVerifiedOrd(ordId);
Board findBoard = findOrd.getProduct().getBoard();
findBoard.getProduct().setLeftStock(findBoard.getProduct().getLeftStock() + findOrd.getQuantity());
boardRepository.save(findBoard);
ordRepository.delete(findOrd);
}
/* 여기에는 OrdService 를 안만들었으므로 여기서 확인 */
public Ord findVerifiedOrd(long ordId) {
Ord findOrd = ordRepository.findById(ordId)
.orElseThrow(() -> new RuntimeException("존재하지 않는 주문입니다."));
return findOrd;
}
/* 서버에 요청할 헤더 */
private HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + adminKey);
headers.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
return headers;
}
}
4️⃣ KakaoPayController 구현
- 결제 준비 요청
- 결제 승인 요청
- 결제 실패
- 결제 취소
@Slf4j
@Controller
@RequiredArgsConstructor
@Validated
public class KakaoPayController {
private final KakaoPayService kakaopayService;
/* 카카오 페이 결제 요청 */
@GetMapping("/order/pay/{ord_id}")
public ResponseEntity payReady(@PathVariable("ord_id") long ordId) {
ReadyResponse response = kakaopayService.payReady(ordId);
return ResponseEntity.ok(response);
}
/* 결제 승인 요청 */
@GetMapping("/order/pay/completed/{ord_id}")
public String payComplete(@RequestParam("pg_token") String pgToken, @PathVariable long ordId) {
/* 카카오 결제 요청 */
OrdResponseDto response = kakaopayService.payApprove(ordId, pgToken);
URI uri = UriComponentsBuilder.fromUri(URI.create("http://localhost:3000/order/pay/completed"))
.queryParam("ordId", ordId)
.build().toUri();
/* 프런트 페이지로 리다이렉트 */
return "redirect:" + uri;
}
/* 결제 취소 요청 -> 주문 내역 삭제 */
@GetMapping("/order/pay/cancel/{ord_id}")
public String payCancel(@PathVariable("ord_id") long ordId) {
kakaopayService.cancelOrFailPayment(ordId);
log.info("결제 취소");
/* 프런트 페이지로 리다이렉트 */
return "redirect:http://localhost:3000/order/pay/cancel";
}
/* 결제 실패 */
@GetMapping("/order/pay/fail/{ord_id}")
public String payFail(@PathVariable("ord_id") long ordId) {
kakaopayService.cancelOrFailPayment(ordId);
log.info("결제 실패");
/* 프런트 페이지로 리다이렉트 */
return "redirect:http://localhost:3000/order/pay/fail";
}
}
'Spring' 카테고리의 다른 글
[Spring] Transaction (0) | 2022.12.19 |
---|---|
[Spring] JDBC와 Spring JDBC, Spring Data JDBC, Spring Data JPA (0) | 2022.12.17 |
[Spring] 예외 처리 - 사용자 정의 (0) | 2022.10.27 |
[Spring] 예외처리 - 공통화(@RestControllerAdvice) (0) | 2022.10.27 |
[Spring] 예외 처리 - Controller에서 처리 (0) | 2022.10.27 |
댓글