import 란?
아임포트는 쇼핑몰 서버를 대신해서 쇼핑몰에서 사용자가 상품을 구매하려 할 때 실제 결제 요청 받는 역할을 하고 이 요청을 받아서 PG(결제 대행 업체)에 요청을 해준다.
그리고 결제 정보 결과에 대해서 응답을 받을 수 있다.
그렇기 때문에 웹 서버의 부담을 줄일 수 있고, PG사 별로 결제를 요청하는 코드를 작성해야하는 번거로움도 사라지게 된다.
iamport는 가이드를 아주 친절하게 제공해주고 무엇보다 한국어로 되어 있어서 보기 편할 것이다.
iamport 가이드
- iamport 개발자 가이드 ( Notion ) : https://guide.portone.io/test_integration
- PG 사별 코드표 : https://portone.gitbook.io/docs/tip/pg-2
- 결제 요청 별 파라미터 : https://portone.gitbook.io/docs/v2-payment/v2-sdk/payment-request#paymethod-string
아임포트 REST API
해당 링크는 아임포트의 API를 테스트 해볼 수 있는 사이트이다.
이 사이트를 통해 요청에 대해 어떠한 응답 데이터를 받아 볼 수 있는지 알 수 있다.
API KEY 발급
위 사이트에 들어가서 회원가입을 해주자.
회원가입이 완료되면 "결제 연동", "상점 - 계정 관리" 탭에서 "내 식별코드 API Keys"를 볼 수 있다.
이 버튼을 클릭하면 다음과 같이 3개의 코드들이 나온다.
이 코드들을 전부 사용해야 하니 이 창을 그대로 띄어둬 놓자.
라이브러리 설치
아임포트는 jquery를 기반으로 하기 때문에 iamport 라이브러리 뿐 아니라 jquery라이브러리도 필요하다.
javascript 라이브러리에 추가
<script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
<script src="https://cdn.iamport.kr/js/iamport.payment-1.1.5.js"></script>
그리고 서버는 Spring을 사용하므로 build.gradle 의존성에 다음 라이브러리를 추가해주자.
build.gradle에 추가
dependencies {
implementation 'com.github.iamport:iamport-rest-client-java:0.1.6'
}
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
참고로 iamport 문서들을 보면 다 maven 기반으로 의존성을 추가하도록 되어있다.
근데 이 기능하나 추가하자고 프로젝트를 maven으로 바꿀수 없기 때문에 알아본 결과 reposioties에 jitpack.io를 추가하면 gradle에서도 iamport 라이브러리를 추가할 수 있었다.
결제 기능 구현하기 (Spring - 서버 코드)
API KEY 추가
먼저 application.properties에 발급받은 REST API Key와 REST API Secret를 작성해 놓고 @Value 어노테이션을 통해 해당 값을 불러오자.
만약 해당 프로젝트가 github과 연동되어 있다면 API KEY는 외부에 노출되선 안되기 때문에 git.ignore 파일에 해당 프로퍼티 파일을 추가 해줘야한다.
IamportClient 초기화
@Controller
@Slf4j
@RequiredArgsConstructor
public class PaymentController {
private IamportClient iamportClient;
@Value("${imp.api.key}")
private String apiKey;
@Value("${imp.api.secretkey}")
private String secretKey;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(apiKey, secretKey);
}
}
IamportClient 객체를 두 API KEY를 인자로 넘겨서 생성하면 이 객체를 통해 iamport 함수들을 사용할 수 있다.
PaymnetController 전체 코드
@Controller
@Slf4j
@RequiredArgsConstructor
public class PaymentController {
private final PaymentService paymentService;
private final RefundService refundService;
private IamportClient iamportClient;
@Value("${imp.api.key}")
private String apiKey;
@Value("${imp.api.secretkey}")
private String secretKey;
@PostConstruct
public void init() {
this.iamportClient = new IamportClient(apiKey, secretKey);
}
@PostMapping("/order/payment")
public ResponseEntity<String> paymentComplete(@Login SessionUser sessionUser, @RequestBody List<OrderSaveDto> orderSaveDtos) throws IOException {
String orderNumber = String.valueOf(orderSaveDtos.get(0).getOrderNumber());
try {
Long userId = sessionUser.getUserIdNo();
paymentService.saveOrder(userId, orderSaveDtos);
log.info("결제 성공 : 주문 번호 {}", orderNumber);
return ResponseEntity.ok().build();
} catch (RuntimeException e) {
log.info("주문 상품 환불 진행 : 주문 번호 {}", orderNumber);
String token = refundService.getToken(apiKey, secretKey);
refundService.refundWithToken(token, orderNumber, e.getMessage());
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}
@PostMapping("/payment/validation/{imp_uid}")
@ResponseBody
public IamportResponse<Payment> validateIamport(@PathVariable String imp_uid) {
IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);
log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse().getMerchantUid());
return payment;
}
}
- @PostMapping("/order/payment")
해당 경로로 요청을 받으면 요청으로 받은 주문 상품들을 저장한다.
만약 저장시에 예외가 발생하면 주문한 상품을 결제 취소하도록 하였다.
결제 취소에 대한 자세한 설명은 아래의 포스팅에서 다룬다.
2023.09.05 - [JAVA/Spring] - [Spring] 아임포트(import)로 결제 취소, 환불 기능 구현하기
- @PostMapping("/payment/validation/{imp_uid}")
여기서 사용된 validateIamport 메서드의 paymentByImpUid()는 imp_uid(결제 고유 ID) 값을 받아 결제 상세 내역을 조회하는 함수이다.
이 imp_uid 값은 발급받은 "가맹정 식별 코드"이고 클라이언트 측에서 요청할 때 넣어줄 것이다.
이 imp_uid 값으로 iamport에 검증 요청을 보내고, 해당 거래의 상세 정보를 조회하고 반환한다.
결제 기능 구현하기 (javascript - 클라이언트 코드)
iamport 예시 코드
IMP.init('가맹점식별코드'); // 가맹점 식별코드로 Iamport 초기화
IMP.request_pay({ // 결제 요청
pg: "danal", // PG사 설정
pay_method : "card", // 결제 방법
merchant_uid : "20230901ABDE", // 주문 번호
name : "상품1", // 상품 이름
amount: 3000, // 결제 가격
buyer_name : "홍길동", // 구매자 이름 (buyer_ 부분은 꼭 작성하지 않아도된다. (선택사항))
buyer_tel : "010-5555-1111", // 구매자 연락처
buyer_postcode : 52030, // 구매자 우편번호
buyer_addr : "경기도 판교" // 구매자 주소
}, function(res) {
if (res.success) {
axios({
method: "post",
url: `/payment/validation/${rsp.imp_uid}`
})
// 응답 데이터의 정보들
console.log("Payment success!");
console.log("Payment ID : " + res.imp_uid);
console.log("Order ID : " + res.merchant_uid);
console.log("Payment Amount : " + res.paid_amount);
} else {
console.error(response.error_msg);
}
});
- IMP.request_pay 함수는 결제를 요청하는 함수이다.
- 사용자의 결제 요청이 성공적으로 이뤄진다면 res.success 구문이 실행되고 반대로 실패할경우 else문을 타게 될 것이다.
- 결제 요청이 성공적으로 이뤄진다면 추가적인 검증 과정을 위해 위에서 작성한 서버측에 해댕경로로 post요청을 한다.
- 이 요청으로 verifyIamport() 함수가 실행되고 결제 내역을 반환해준다.
- 결제 내역이 반환된다면 결제는 성공적으로 수행되어 해당 응답 데이터의 로그가 콘솔에 보이게 될것이다.
전체 js 코드 (jquery로 작성)
아래의 코드는 이번 프로젝트에서 사용한 코드의 일부이다.
간단하게 3개의 결제수단만이 있고 각 버튼을 클릭할때마다 iamport에 결제 요청을 할 데이터가 변경되어야 하므로 따로 변수로 빼서 작성하였다.
간단한 로직을 설명하면 아래와 같다.
- 결제하기 버튼을 클릭 후, 결제 요청에 성공하면 장바구니에 있는 상품들을 orders에 담아 " /user/order/add" 경로로 요청하여 주문 테이블에 해당 상품들을 저장.
- 성공적으로 저장이 끝나면 "/cart/delete/pay/success" 경로로 요청해 주문한 상품들을 장바구니에서 제거
- 모든 요청 성공적으로 반환 후 "결제 완료" 알림창 출력.
$(() =>{
let pg = "";
let payMethod = "";
$('#cartPay').on("click", () => {
pg = "html5_inicis";
payMethod = "card"
});
$('#phonePay').on("click", () => {
pg = "danal";
payMethod = "MOBILE";
});
$('#kakaoPay').on("click", () => {
pg = "kakaopay";
payMethod = "card"
});
$(document).on("click", "#pay__do", () => {
let name = $("#orderProductNames").text();
let orders = [];
const orderNumber = createOrderNum();
$('.orderProductPrice').each((i, e) => {
let order = {};
const price = parseInt($(e).text().replace(/[^0-9]/g, ''));
let discountRate = parseInt($('.orderProductDiscount').eq(i).text().replace(/[^0-9]/g, ''));
if (!discountRate) {
discountRate = 0;
}
order.productId = $(".td_wrap").eq(i).data("order-product-id")
order.orderPrice = price - (price * (discountRate / 100));
order.orderCount = parseInt($('.orderProductCount').eq(i).text().replace(/[^0-9]/g, ''));
order.receiverName = $("#name").val();
order.phoneNumber = $("#phone1").val();
order.orderNumber = orderNumber;
order.zipcode = parseInt($("#add").val());
order.address = $("#add2").val();
order.orderRequired = $("#ask").val();
order.paymentMethod = payMethod;
orders.push(order);
});
// iamport 초기화 및 결제 요청
IMP.init('가맹점식별코드');
IMP.request_pay({
pg: pg,
pay_method : payMethod,
merchant_uid : orderNumber,
name : name,
amount: $("#paymentPrice").val(), // 결제 가격
buyer_name : $('#name').val(),
buyer_tel : $('#phone1').val(),
buyer_postcode : $('#add').val(),
buyer_addr : $('#add2').val()
}, function(rsp) {
if (rsp.success) {
axios({
method: "post",
url: `/payment/validation/${rsp.imp_uid}`
}).then(res => {
if ($("#paymentPrice").val() == res.data.response.amount) {
axios({
url: "/order/payment",
method: "post",
data: orders,
dataType: "json",
headers: {'Content-Type': 'application/json'}
}).then(res => {
let productIds = orders.map(order => order.productId);
axios({
url: `/cart/delete/pay/success`,
method: "post",
data: {
userIdNo: res.data,
productIds: productIds
}
})
let msg = '결제가 완료되었습니다.';
msg += '고유ID : ' + rsp.imp_uid;
msg += '상점 거래ID : ' + rsp.merchant_uid;
msg += '결제 금액 : ' + rsp.paid_amount;
msg += '카드 승인번호 : ' + rsp.apply_num;
alert(msg)
}).catch(error => {
alert("주문정보 저장을 실패 했습니다.")
});
}
}).catch(error => {
alert('결제에 실패하였습니다. ' + rsp.error_msg);
});
} else {
alert(rsp.error_msg);
}
});
})
});
// 주문번호 생성 함수
function createOrderNum() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
let orderNum = year + month + day;
for (let i = 0; i < 5; i++) {
orderNum += Math.floor(Math.random() * 8);
}
return parseInt(orderNum);
}
결제 취소 기능 구현하기
아래의 포스팅을 통해 결제 취소 기능을 구현하는 방법에 대해 상세히 알아볼 수 있다.
'◼ Spring' 카테고리의 다른 글
[Spring + Redis] Redis cache를 적용해 조회 성능 개선 방법 (0) | 2023.09.11 |
---|---|
[Spring] 아임포트(import)로 결제 취소, 환불 기능 구현하기 (6) | 2023.09.05 |
[Spring] 로그인 어노테이션으로 세션 정보가져오기 (Argument Resolver 활용) (0) | 2023.08.31 |
[Spring] Cookie(쿠키)를 활용한 최근 본 상품 기능 구현 (Session과 비교) (0) | 2023.08.30 |
[Spring] 공통 기능을 @Aspect를 사용해 적용하기 (0) | 2023.08.30 |