💡 PG사란 ?
: Payment Gateway 의 줄임말
구매자와 판매자 사이에서의 이뤄지는 결제를 안전하게 할 수 있도록 대행해주는 역할을 담당
대표적인 PG사로는 KG 이니시스, NHN, KCP, LGU+ 등이 있으며,
모바일 환경으로는 KG 모빌리언스, 다날, 카카오Pay 등이 있다 .
포트원: https://admin.portone.io/
위 주소로 들어가서 회원가입 후 '결제연동' 탭으로 이동후 원하는 결제대행사 테스트로 추가한다
나는 KG이니시스를 선택하였다
사실 프론트의 결제 연동하는 코드는 위에 올렸던 메뉴바 가장 아래에 위치해있는 '콘솔가이드'에 자세히 나와있다
https://developers.portone.io/docs/ko/auth/guide/readme?v=v1
아래는 인증결제가 어떻게 이루어지는지 잘 설명되어있는 그림이다
나는 백엔드 쪽을 개발하는거라 프론트 코드는 위 사이트에 들어가서 참고하면 될것 같다.
(우리 프론트 팀원도 해당 문서에 나와있는 코드를 거의 그대로 썻다고 한다!)
프로젝트를 시작하기 전에 실제 pg창이 뜨는지 궁금해서 간단하게 적어본 html은 아래와 같았다.
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
</head>
<body> <!-- jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js"></script> <!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
<div><h2>IAMPORT 결제 데모</h2>
<li>
<button id="iamportPayment" type="button">결제테스트</button>
</li>
</div>
<script>
//문서가 준비되면 제일 먼저 실행
$(document).ready(function () {
$("#iamportPayment").click(function () {
proceedPay(); //버튼 클릭하면 호출
});
})
function proceedPay() {
$.ajax({
url: '/payment/proceed',
type: 'POST',
async: true,
dataType: "Json",
data:
$('#orderForm').serialize(),
success: function (data) {
if (data.cnt > 0) {
requestPay(data)
} else {
alert(data.msg)
}
},
error: function (e) {
alert("에러")
}
});
}
function requestPay(data) {
IMP.init("가맹점 코드"); // 예: imp00000000
//IMP.request_pay(param, callback) 결제창 호출
IMP.request_pay({ // param
pg: "html5_inicis.INIBillTst", //결제대행사 설정에 따라 다르며 공식문서 참고
pay_method: "card", //결제방법 설정에 따라 다르며 공식문서 참고
merchant_uid: data.no, //주문(db에서 불러옴) 고유번호
name: data.products,
amount: data.price,
buyer_email: "",
buyer_name: data.name,
//buyer_tel: "010-4242-4242",
buyer_addr: data.addr,
//buyer_postcode: "01181"
}, function (rsp) { // callback
if (rsp.success) {
// 결제 성공 시: 결제 승인 또는 가상계좌 발급에 성공한 경우
// jQuery로 HTTP 요청
jQuery.ajax({
url: "/payment/verify/" + rsp.imp_uid,
method: "POST",
}).done(function (data) {
// 위의 rsp.paid_amount 와 data.response.amount를 비교한후 로직 실행 (iamport 서버검증)
if (rsp.paid_amount == data.response.amount) {
succeedPay(rsp.imp_uid, rsp.merchant_uid);
} else {
alert("결제 검증 실패");
}
})
} else {
var msg = '결제에 실패하였습니다.';
msg += '에러내용 : ' + rsp.error_msg;
alert(msg);
}
});
}
</script>
</body>
</html>
build.gradle에 포트원 관련 설정을 추가하였다.
repositories {
mavenCentral()
//iamport
maven {
url 'https://jitpack.io'
}
}
dependencies {
//iamport
implementation 'com.github.iamport:iamport-rest-client-java:0.2.23'
}
추가로 아래 결제 검증을 할 때 필요한 설정 파일들을 만들어주었다. 이때 impKey
, impSecretKey
는 유출되면 안된다고 하여 properties 파일에 숨겨서 사용하였다.
IamportApiProperty.java
@Getter
@Configuration
@PropertySource("classpath:/secret.properties")
public class IamportApiProperty {
@Value("${imp_key}")
private String impKey;
@Value("${imp_secret}")
private String impSecret;
}
IamportConfig.java
@Configuration
@RequiredArgsConstructor
public class IamportConfig {
private final IamportApiProperty iamportApiProperty;
@Bean
public IamportClient iamportClient() {
return new IamportClient(iamportApiProperty.getImpKey(), iamportApiProperty.getImpSecret());
}
}
문서를 참고하면서 개발하다보면 결제정보를 검증하는 과정이 꼭 필요하다고 나와있었고 그 이유도 잘 나와있어서 우리도 결제정보를 검증하는 과정을 추가하게 되었다.
위 화면에서 보면 백엔드에서 검증은 총 2번 1. 사전검증, 2. 사후검증을 진행하도록 되어있다.
사전검증
결제금액 사전등록 API를 요청하여 포트원 측에 주문아이디와 가격을 등록한다
https://developers.portone.io/api/rest-v1/payment.validation?v=v1#post%20%2Fpayments%2Fprepare
IamportRepository.java
public void prepare(Long orderId, BigDecimal price) {
try {
iamportClient.postPrepare(new PrepareData(String.valueOf(orderId), price));
} catch (Exception e) {
throw new CustomException(ErrorCode.IAMPORT_ERROR);
}
}
사후 검증
1. 사전 검증 이후에 프론트 측에서 결제를 끝내고 나면 받게되는 포트원 결제고유번호(imp_uid), 가맹점 주문번호(merchant_uid)를 프론트엔드로부터 수신
2. 결제 상세내역 조회를 위해 포트원 결제 단건 조회 API 요청
3. 응답받은 내용을 바탕으로 실 결제 금액과 결제요청금액(가맹점 자체 데이터베이스)을 비교
사후 검증은 결제가 이루어지고 난 다음에 우리의 데이터베이스에 저장된것과 비교하여 결제가 잘 이루어 진게 맞는지 검증하는 과정이라 위 절차를 순서대로 진행하면 된다.
나는 PaymentRequest
의 결제고유번호(imp_uid), 가맹점 주문번호(merchant_uid)를 받고, 포트원 결제 단건 조회 API 요청하여 주문테이블에 저장되어있는 정보와 비교하여 정상적인 결제가 이루어졌다고 생각하면 결제테이블에 단건 조회 결과 = 결제 정보 를 저장하고 있다.(우리의 서비스가 복잡하여 실제로는 상태값들을 조회하는 과정이 많이 포함되어 있어서 실제 저장하고 있는 코드는 다른 글에서 다룰 예정이다!)
IamportRepository.java
public Optional<Payment> findPaymentByImpUid(String impUid) {
try {
IamportResponse<com.siot.IamportRestClient.response.Payment> response =
iamportClient.paymentByImpUid(impUid);
return Optional.ofNullable(paymentMapper.mapFrom(response.getResponse()));
} catch (Exception e) {
throw new CustomException(ErrorCode.IAMPORT_ERROR);
}
}
이렇게 하면 간단한 결제 구현은 할 수 있다 😉