[Spring Boot] Portone(Iamport) 포트원으로 결제 구현
개요
Spring 프로젝트를 진행하면서 결제 구현이 필요했다.
실제 PG 사와 카드사를 거쳐 구현하기에는 까다로운 요소들이 많았고, 이를 간단하게 구현할 수 있는 PortOne API 를 사용하기로 하였다.
해당 게시물은 Spring Boot 만을 사용하였다.
코드
1. Front
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Payment Page</title>
<!-- jQuery CDN 추가 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- 아임포트 스크립트 추가 -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
</head>
<body>
<!-- 버튼들 -->
<button id="cardPay" onclick="handlePayment('html5_inicis.INIpayTest', 'card')">카드 결제</button>
<button id="kakaoPay" onclick="handlePayment('kakaopay', 'card')">카카오페이 결제</button>
<script th:inline="javascript">
// 아임포트 코드
var impCode = /*[[${@environment.getProperty('imp.code')}]]*/ '';
function handlePayment(pg, payMethod) {
console.log("handlePayment");
console.log(pg);
console.log(payMethod);
var order = {
productId: 1,
productName: '상품1',
price: 3000,
quantity: 1
};
// 결제하기 버튼 클릭 시 결제 요청
IMP.init(impCode);
IMP.request_pay({
pg: pg,
pay_method: payMethod,
merchant_uid: '212R3A11TD233AAC', // 주문번호 생성
name: '상품1',
amount: 3000, // 결제 가격
buyer_name: '김민규',
buyer_tel: '010-1234-5678'
}, function(rsp) {
if (rsp.success) {
// 결제 성공 시
$.ajax({
type: 'POST',
url: 'api/v1/payment/validation/' + rsp.imp_uid
}).done(function(data) {
console.log(data);
if (order.price == data.response.amount) {
order.impUid = rsp.imp_uid;
order.merchantUid = rsp.merchant_uid;
// 결제 금액 일치. 결제 성공 처리
$.ajax({
url: "api/v1/payment/order",
method: "post",
data: JSON.stringify(order),
contentType: "application/json"
}).then(function(res) {
console.log("res", res);
console.log("rsp", rsp);
var msg = '결제가 완료되었습니다.';
msg += '고유ID : ' + rsp.imp_uid;
msg += '상점 거래ID : ' + rsp.merchant_uid;
msg += '결제 금액 : ' + rsp.paid_amount;
msg += '카드 승인번호 : ' + rsp.apply_num;
alert(msg);
}).catch(function(error) {
alert("주문정보 저장을 실패 했습니다.");
});
}
}).catch(function(error) {
alert('결제에 실패하였습니다. ' + rsp.error_msg);
});
} else {
alert(rsp.error_msg);
}
});
}
</script>
</body>
</html>
결제는 클라이언트가 직접 portone 에 결제 요청하면서 시작된다.
타임리프로 구성된 페이지를 자세히 살펴보면 총 3가지 단계로 구성되어있다.
- 포트원 라이브러리를 추가하고 객체를 초기화한다.
- 서버에서 실제 결제 건을 조회한다.
- 결제 금액이 일치한다면 Order 객체를 서버에 저장한다.
클라이언트 측에서 라이브러리를 다운받기만 하더라도 다음과 같은 결제창을 얻을 수 있다.
2. Controller
@RestController
@RequestMapping("/api/v1/payment")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "Payments", description = "결제 API")
public class PaymentController {
private final PaymentService paymentService;
@PostMapping("/validation/{imp_uid}")
public IamportResponse<Payment> validateIamport(@PathVariable String imp_uid) throws IamportResponseException, IOException {
log.info("imp_uid: {}", imp_uid);
log.info("validateIamport");
return paymentService.validateIamport(imp_uid);
}
@PostMapping("/order")
public ResponseEntity<String> processOrder(@RequestBody OrderDto orderDto) {
// 주문 정보를 로그에 출력
log.info("Received orders: {}", orderDto.toString());
// 성공적으로 받아들였다는 응답 반환
return ResponseEntity.ok(paymentService.saveOrder(orderDto));
}
@PostMapping("/cancel/{imp_uid}")
public IamportResponse<Payment> cancelPayment(@PathVariable String imp_uid) throws IamportResponseException, IOException {
return paymentService.cancelPayment(imp_uid);
}
}
3.Service
@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {
private final IamportClient iamportClient;
private final OrderRepository orderRepository;
/**
* 아임포트 서버로부터 결제 정보를 검증
* @param imp_uid
*/
public IamportResponse<Payment> validateIamport(String imp_uid) {
try {
IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);
log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse());
return payment;
} catch (Exception e) {
log.info(e.getMessage());
return null;
}
}
/**
* 아임포트 서버로부터 결제 취소 요청
*
* @param imp_uid
* @return
*/
public IamportResponse<Payment> cancelPayment(String imp_uid) {
try {
CancelData cancelData = new CancelData(imp_uid, true);
IamportResponse<Payment> payment = iamportClient.cancelPaymentByImpUid(cancelData);
return payment;
} catch (Exception e) {
log.info(e.getMessage());
return null;
}
}
/**
* 주문 정보 저장
* @param orderDto
* @return
*/
public String saveOrder(OrderDto orderDto){
try {
orderRepository.save(orderDto.toEntity());
return "주문 정보가 성공적으로 저장되었습니다.";
} catch (Exception e) {
log.info(e.getMessage());
cancelPayment(orderDto.getImpUid());
return "주문 정보 저장에 실패했습니다.";
}
}
}
우선 Config 를 통해 IamportClient 를 Bean 으로 등록한다.
이전에는 token 을 발급받고 특정 url 을 통해 결제 조회 및 취소를 진행했지만 지금은 IamportClient 를 통해 간편하게 조회 및 취소가 가능하다.
추가
PG 테스트 설정
해당 에러가 발생했었다. 이는 PortOne 에서 PG 설정을 등록하지 않았기 때문인다.
테스트를 위해서 혹은 실제 결제를 위해 등록 과정이 필요하다.
댓글남기기