[Spring] 아임포트(import)로 결제 기능 구현하기 (클라이언트 + 서버 코드 포함)

import 란?

아임포트는 쇼핑몰 서버를 대신해서 쇼핑몰에서 사용자가 상품을 구매하려 할 때 실제 결제 요청 받는 역할을 하고 이 요청을 받아서 PG(결제 대행 업체)에 요청을 해준다.

그리고 결제 정보 결과에 대해서 응답을 받을 수 있다.

그렇기 때문에 웹 서버의 부담을 줄일 수 있고, PG사 별로 결제를 요청하는 코드를 작성해야하는 번거로움도 사라지게 된다.

PG사를 카카오페이로 사용했을 때 화면

 

iamport는 가이드를 아주 친절하게 제공해주고 무엇보다 한국어로 되어 있어서 보기 편할 것이다.

iamport 가이드

 

아임포트 REST API

해당 링크는 아임포트의 API를 테스트 해볼 수 있는 사이트이다.

이 사이트를 통해 요청에 대해 어떠한 응답 데이터를 받아 볼 수 있는지 알 수 있다.

 

API-포트원

 

api.iamport.kr

 


API KEY 발급

 

포트원 관리자콘솔

단 하나의 솔루션, 결제, 그 이상의 경험

admin.portone.io

위 사이트에 들어가서 회원가입을 해주자.

 

회원가입이 완료되면 "결제 연동", "상점 - 계정 관리" 탭에서 "내 식별코드 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);
            }
        });
  1. IMP.request_pay 함수는 결제를 요청하는 함수이다.
  2. 사용자의 결제 요청이 성공적으로 이뤄진다면 res.success 구문이 실행되고 반대로 실패할경우 else문을 타게 될 것이다.
  3. 결제 요청이 성공적으로 이뤄진다면 추가적인 검증 과정을 위해 위에서 작성한 서버측에 해댕경로로 post요청을 한다.
  4. 이 요청으로  verifyIamport() 함수가 실행되고 결제 내역을 반환해준다.
  5. 결제 내역이 반환된다면 결제는 성공적으로 수행되어 해당 응답 데이터의 로그가 콘솔에 보이게 될것이다.

 

전체 js 코드 (jquery로 작성)

아래의 코드는 이번 프로젝트에서 사용한 코드의 일부이다.

간단하게 3개의 결제수단만이 있고 각 버튼을 클릭할때마다 iamport에 결제 요청을 할 데이터가 변경되어야 하므로 따로 변수로 빼서 작성하였다.

간단한 로직을 설명하면 아래와 같다.

  1. 결제하기 버튼을 클릭 후, 결제 요청에 성공하면 장바구니에 있는 상품들을 orders에 담아 " /user/order/add" 경로로 요청하여 주문 테이블에 해당 상품들을 저장.
  2. 성공적으로 저장이 끝나면 "/cart/delete/pay/success" 경로로 요청해 주문한 상품들을 장바구니에서 제거 
  3. 모든 요청 성공적으로 반환 후 "결제 완료" 알림창 출력.
$(() =>{
    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] 아임포트(import)로 결제 취소, 환불 기능 구현하기

결제 취소 기능에 대해 포스팅하기 앞서 iamport를 사용하기 위한 준비과정 및 결제 기능 구현하는 방법에 대해서는 아래 포스팅에 설명되어 있다. [Spring] 아임포트(import)로 결제 기능 구현하기 (

hstory0208.tistory.com