• 아임포트(iamport) API로 결제부터 취소까지

    2022. 1. 25.

    by. 와트

    쇼핑몰 관련 페이지를 작업하게 되면서 결제 관련 API를 사용해야 할 때가 되었다.

    가장 자주 쓰이는 iamport API를 선택했다.

    https://www.iamport.kr/

     

    온라인 비즈니스의 모든 결제를 한곳에서, 아임포트

    결제의 시작부터 비즈니스의 성장까지 아임포트와 함께하세요

    www.iamport.kr

     

    아임포트

    무료로 서비스되는 결제 연동 API로 PG 계약과 관계없이 즉시 개발 가능하며, 웹, 앱 SDK 모두 지원한다.

    국내외 여러 PG(복수 선택 가능)와 결제수단을 소스코드 한 줄로 사용할 수 있으며, PG사 변경으로 인한 개발이 필요 없다.


    사용하기 전, 회원가입을 진행한다.

    사용 계정은 프로젝트 공용으로 되어 있는 메일 계정을 사용했다.

    시스템 설정의 PG설정에서 어떤 PG사를 선택할 것인지 골라주고 테스트모드를 ON으로 변경한다.

    테스트계정으로 결제를 진행하는 경우, 매일 자정이 되기 전 자동 취소가 이뤄진다!

     

    결제의 경우 iamport가 제공하는 결제 url로 결제 요청을 보내면 된다.

    하지만 여기서 고려해야 할 몇 가지가 있었다.

    • 결제 금액에 대한 검증 절차를 어떻게 밟을 것인지
    • 결제 도중 오류 발생 시 대처를 어떻게 할 것인지

    결제 검증하기

    이 경우는 구매 금액과 iamport url을 통해 결괏값으로 돌려 받은 결제 금액을 비교하면 되었다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    function iamport(data){
        //가맹점 식별코드
        IMP.init('imp식별코드');
        IMP.request_pay({
            pg : 'kcp',
            pay_method : 'card',
            merchant_uid : data.merchantUid,
            name : $(".merchant-title").text(), //결제창에서 보여질 이름
            amount : 200//실제 결제되는 가격
            //amount : $("[name=totalPrice]").val(), //실제 결제되는 가격
            buyer_email : $(".email-input").text(),
            buyer_name : $(".name-input").text(),
            buyer_tel : $(".phone-input").text(),
            buyer_addr : $(".add-input").text()+$(".add-detail-input").val(),
            buyer_postcode : $("[name=postcode]").val()
        }, function(rsp) {
            console.log(rsp);

            $.ajax({
                type : "POST",
                url : "${pageContext.request.contextPath}/verifyIamport/" + rsp.imp_uid 
            }).done(function(data) {
                
                console.log(data);
                
                // 위의 rsp.paid_amount 와 data.response.amount를 비교한후 로직 실행 (import 서버검증)
                if(rsp.paid_amount == data.response.amount){
                    console.log("결제 및 결제검증완료");
                    //결제 및 검증 완료 시 결제 테이블 정보 추가 후 주문 정보 디테일 페이지로 리다이렉트
                    let value = {
                        impUid : rsp.imp_uid,
                        merchantUid : rsp.merchant_uid,
                        name : data.response.name,
                        payMethod : data.response.payMethod,
                        pgProvider : data.response.pgProvider,
                        amount : data.response.amount,
                        buyerAddr : data.response.buyerAddr,
                        buyerEmail : data.response.buyerEmail,
                        buyerName : data.response.buyerName,
                        buyerPostcode : data.response.buyerPostcode,
                        buyerPhone : data.response.buyerTel
                    };
                    value = JSON.stringify(value);
                    console.log(value);
                    
                    
                    $.ajax({
                        url : "${pageContext.request.contextPath}/order/impEnroll",
                        data : value,
                        method : "POST",
                        contentType : "application/json; charset=utf-8",
                        success(data){
                            console.log(data);
                            location.href = `${pageContext.request.contextPath}\${data}`;
                        },
                        error : console.log
                    });
                    
                    
                } else {
                    alert("결제에 실패하였습니다.");
                }
            });
        });
    }
    cs

     

    3번째 줄의 imp 식별코드는 iamport 관리자 페이지의 시스템 설정>내정보에 있는 가맹점 식별코드를 입력하면 된다.

    IMP.request_pay를 통해 iamport에 넘길 결제 정보들을 지정한다.

    이후 /verifyIamport url로 결제를 요청하고, 결괏값으로 data를 돌려 받는다.

    앞서 결제 정보 관련 입력값이 담긴 객체 rsp와 실제 결제 정보가 담긴 객체 data 안의 결제 금액을 비교하여, 해당 금액이 일치하면 성공적으로 결제한 것으로 검증을 완료한다.


    결제 도중 오류 발생

    네트워크 등의 문제로 인해 결제를 진행하다가 실패하는 경우를 대비하기 위해 로직을 다음과 같이 짰다.

    1. 사용자가 결제 버튼을 누른다.
    2. 버튼을 누르는 순간 주문 테이블에 사용자의 주문 정보를 insert한다.
    3. 테이블에 성공적으로 insert하면 그때 결제를 진행한다(도중에 에러가 발생하더라도 주문 관련 정보는 DB에 남아 있다)
    4. 결제가 성공하면 결제 테이블에 사용자의 결제 정보를 insert한다.

    모든 요청은 비동기로 처리되어, 총 3번의 비동기 요청이 들어갔다.


     

    결제 취소

    결제 취소의 경우 로직을 꾸리는 데에 조금 더 애를 먹었는데, 취소 관련 url을 클라이언트단에서 요청하는 것이 아니라 서버단에서 요청해야 했기 때문이다.

    secret API key 정보를 넘기고, 토큰도 발급 받아 넘겨야 했는데 이런 일련의 과정을 클라이언트단에서 하면 공격에 취약한 코드가 될 것이라 생각했다.

    여기에 더해 사용자가 요청한 주문의 상태가 상품 준비중 이전/배송시작 이후로 분리되어 전자의 경우는 바로 주문취소가 가능하도록 했으며, 후자의 경우는 제품을 돌려 받은 것을 관리자가 확인한 이후에야 결제 환불이 가능하도록 해야 했다.

    주문 상태/상품 처리 상태에 따라 분기 처리가 필요했다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    @PostMapping("/orderLogEnroll")
        public String orderLogEnroll(OrderLog orderLog, HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttr) {
            log.debug("orderLog = {}", orderLog);
                try {
                    int result = orderService.insertOrderLog(orderLog);
                    if(result > 0 ) {
                        //주문 취소 건의 경우 곧바로 환불 가능한 access token 발급
                        //보안상의 문제로 서버 사이드에서 요청 보낼 것
                        if("CAN".equals(orderLog.getCsStatus())){
                            Imp imp = orderService.selectOneImp(orderLog.getMerchantUid());
                            // 아임포트 토큰생성 
                            String requestUrl = "https://api.iamport.kr/users/getToken";
                            String imp_key = URLEncoder.encode("imp key""UTF-8");
                            String imp_secret = URLEncoder.encode("imp secret key""UTF-8");
     
                            JSONObject json = new JSONObject();
                            json.put("imp_key", imp_key);
                            json.put("imp_secret", imp_secret);
                            String _token = AdminUtils.getToken(request, response, json, requestUrl);
                            log.debug("token = {}", _token);
                            
                            JSONObject json2 = new JSONObject();
                            json2.put("reason", orderLog.getReasonDetail());
                            json2.put("imp_uid", imp.getImpUid());
                            json2.put("amount", imp.getAmount());
                            
                            Map<String, Object> map = AdminUtils.getRefund(request, response, json2, _token);
                            log.debug("cancelResult = {}", map.get("receipt"));
                            String receipt = (String)map.get("receipt");
                            
                            redirectAttr.addFlashAttribute("msg", (String)map.get("message"));
                            
                            if(receipt != null) {
                                Map<String, Object> param = new HashMap<>();
                                String[] uidArr = new String[1];
                                uidArr[0= orderLog.getOrderLogUid();
                                param.put("keyword""END_DATE");
                                param.put("orderLogUid", uidArr);
                                param.put("receiptUrl", receipt);
                                result = orderService.updateOrderLog(param);
                            }
                        }
                    }
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            return "redirect:/mypage/changeOrderList.do";
        }
    cs

    Controller에서 imp key와 imp secret key를 이용해 토큰을 발행하고,

    결제 취소에 필요한 정보를 JSONObject에 담아 iamport 취소 url로 취소 요청을 보냈다.

    두 메소드 모두 관리자측에서도 사용할 일이 있기 때문에 static 메소드로 별도로 분리해 놓았다.

    취소가 성공적으로 될 경우 영수증 url이 리턴된다.

    그 정보를 DB에 저장하고 주문 변동 테이블의 주문 상태를 업데이트했다.

    댓글

Designed by Nana