[스프링부트 + JPA + 타임리프] Iamport API 이용한 강의 결제 구현 & 서버 검증

jyleever·2022년 5월 23일
6

강의를 구매하기 위해 결제 API인 아임포트 API 를 이용하여 결제를 구현해보았다.

  1. https://www.iamport.kr/
    회원가입하여 계정을 생성하고 키를 발급받는다.


  1. 아임포트에서 제공하는 javascript sdk 라이브러리 추가
    <!-- 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.1.8.js"></script>

결제 화면을 구현하기 앞서 서버에서 결제를 검증하는 로직을 추가한다.

서버에서 결제 검증

개발자들이 사용하기 쉽게 자바스크립트 형태로 제공하고 있기 때문에 결제 금액 및 결제 상태에 대한 변조가 가능하다.
따라서 요청한 금액과 결제가 올바르게 이루어졌는지에 대해 아임 포트 서버로 아임포트 거래 고유번호(imp_uid)나 주문 고유번호(merchant_uid)를 보내 확인하는 과정을 반드시 거쳐야 한다.

1. 아임포트 관리자 페이지 설정

Rest Api를 통해 결제 검증 및 취소가 이루어진다. 이루어지기 앞서 Token을 발급받아야 한다. Token을 발급받기 위해 관리자 페이지의 시스템 설정에서 REST API키와 REST API secret을 복사해놓도록 한다.

2. JAVA SPRING 설정 (gradle)

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
  • 의존성 추가
	dependencies {
		implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
	}

3. 서버 검증

  • 서버 검증을 담당하는 컨트롤러를 생성한다.

VerifyController

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/verifyIamport")
public class VerifyController {

    /** Iamport 결제 검증 컨트롤러 **/
    private final IamportClient iamportClient;

    // 생성자를 통해 REST API 와 REST API secret 입력
    public VerifyController(){
        this.iamportClient = new IamportClient("...", "...");
    }

    /** 프론트에서 받은 PG사 결괏값을 통해 아임포트 토큰 발행 **/
    @PostMapping("/{imp_uid}")
    public IamportResponse<Payment> paymentByImpUid(@PathVariable String imp_uid) throws IamportResponseException, IOException{
        log.info("paymentByImpUid 진입");
        return iamportClient.paymentByImpUid(imp_uid);
    }

}
  • 검증에 필요한 자바 함수는 paymentByImpUid
  • paymentByImpUid
    를 사용하기 위해서는 토큰 발급이 필요하고, 토큰 발급을 하기 위해서는 REST API 키와 REST API secret가 필요하다.
  • 생성자를 통해 REST API 와 REST API secret 를 주입하여 토큰발급을 용이하게 했다.

paymentByImpUid 함수는 아임포트서버에서 imp_uid(거래 고유번호)를 검사하여, 데이터를 보내준다.

결제 화면 구현 - javascript

    /** 결제 **/
    // 결제 금액, 구매자의 이름, 이메일
    const priceAmount = $('#totalPrice').val();
    const buyerMemberEmail = $('#memberEmail').val();
    const buyerMemberName = $('#memberName').val();
    // const form = document.getElementById("payment");

    console.log(priceAmount);
    console.log(buyerMemberName);
    console.log(buyerMemberEmail);
    const IMP = window.IMP;
    IMP.init('imp99053400');

    function requestPay() {
        // IMP.request_pay(param, callback) 결제창 호출
        IMP.request_pay({ // param
            pg: "kakaopay.TC0ONETIME",
            pay_method: "card",
            merchant_uid: 'cart_' + new Date().getTime(),
            name: "Helpring 강의",
            amount: priceAmount,
            buyer_email: buyerMemberEmail,
            buyer_name: buyerMemberName,

        }, function (rsp) { // callback

            /** 결제 검증 **/
            $.ajax({
                type: 'POST',
                url: '/verifyIamport/'+rsp.imp_uid,
                beforeSend: function(xhr){
                    xhr.setRequestHeader(header, token);
                }
            }).done(function(result){

                // rsp.paid_amount와 result.response.amount(서버 검증) 비교 후 로직 실행
                if(rsp.paid_amount === result.response.amount){
                    alert("결제가 완료되었습니다.");
                    $.ajax({
                        type:'POST',
                        url:'/lecture/payment',
                        beforeSend: function(xhr){
                            xhr.setRequestHeader(header, token);
                        }
                    }).done(function() {
                        window.location.reload();
                    }).fail(function(error){
                            alert(JSON.stringify(error));
                    })
                } else{
                    alert("결제에 실패했습니다."+"에러코드 : "+rsp.error_code+"에러 메시지 : "+rsp.error_message);

                }
            })
        });
    };

가맹점 식별코드

    const IMP = window.IMP;
    IMP.init('__');

Iinit 함수의 인자에 가맹점 식별코드를 입력한다. 가맹점 식별코드는 아임포트 홈페이지 -> 대시보드 -> 로그인 -> 시스템 설정에서 찾을 수 있다.

결제창 호출 코드 추가하기

IMP.request_pay({ // param
        pg: "kakaopay.TC0ONETIME",
        pay_method: "card",
        merchant_uid: 'cart_' + new Date().getTime(),
        name: "Helpring 강의",
        amount: priceAmount,
        buyer_email: buyerMemberEmail,
        buyer_name: buyerMemberName
}

IMP.request_pay(param, callback)을 호출하는 코드를 작성

함수의 첫 번째 인자인 param에 결제 요청에 필요한 속성과 값을 담는다. 해당 함수를 호출하면 입력한 속성과 값에 따라 결제창을 보여주며, 따라서 파라미터들은 운영하는 서비스/환경에 맞게 설정해야 한다.

결제 완료 후 실행 콜백 함수

        }, function (rsp) { // callback

            /** 결제 검증 **/
            $.ajax({
                type: 'POST',
                url: '/verifyIamport/'+rsp.imp_uid,
                beforeSend: function(xhr){
                    xhr.setRequestHeader(header, token);
                }
            }).done(function(result){

                // rsp.paid_amount와 result.response.amount(서버 검증) 비교 후 로직 실행
                if(rsp.paid_amount === result.response.amount){
                    alert("결제가 완료되었습니다.");
                    $.ajax({
                        type:'POST',
                        url:'/lecture/payment',
                        beforeSend: function(xhr){
                            xhr.setRequestHeader(header, token);
                        }
                    }).done(function() {
                        window.location.reload();
                    }).fail(function(error){
                            alert(JSON.stringify(error));
                    })
                } else{
                    alert("결제에 실패했습니다."+"에러코드 : "+rsp.error_code+"에러 메시지 : "+rsp.error_message);

                }
            })

사용자가 결제 완료 후 실행되는 함수

callback함수로 전달되는 rsp 인자에는 결제의 성공 여부, 결제 정보, 에러 정보 등을 가지고 있다.
결제 프로세스 완료 후 rsp인자를 통해 결과를 확인하고, 확인 이후에 수행하고자 하는 작업을 callback에서 작성한다.

금액 비교 검증

  • 콜백 함수가 실행되고 return 된 value(rsp) 속 객체에 실제 결제한 금액(rsp.paid.amount)이 return 된다.
  • 콜백 함수가 실행되면, rsp.imp_uid(거래 고유번호)를 통해 AJAX로 검증을 실행한다.
  • paid_amountrsp.paid.amount를 비교하여 검증을 완료한다. 검증이 완료되면 해당 구매자에게 해당 강의를 수강할 수 있도록 권한을 부여한다.(lecture/payment에 post 방식으로 ajax 통신)

결제 화면






에러

큐알코드 인증 시 '종료된 요청입니다.' 알림이 뜨면서 결제가 안 됨

  • 결제 화면에서 사용자가 QR 링크에 접속한 것을 감지하기 전에 결제가 완료되면 발생하는 문제
  • QR 코드가 아닌 직접 휴대폰 번호와 생년월일을 입력함으로서 결제 해결

내가 수강하고 있는 강의로 리다이렉트가 되지 않음

  • 서버에서 내 강의로 리다이렉트하도록 설계했는데 왜 리다이렉트가 되지 않은 걸까? 그래서 우선 자바스크립트 내에서 결제가 성공되면 window.location.reload() 하는 걸로 설계를 변경했다.

출처
https://docs.iamport.kr/implementation/payment#add-library
https://tyrannocoding.tistory.com/44
https://github.com/iamport/iamport-manual/blob/master/%EC%9D%B8%EC%A6%9D%EA%B2%B0%EC%A0%9C/sample/kakao.md
https://jee2memory.tistory.com/entry/%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EC%B9%B4%EC%B9%B4%EC%98%A4%EC%A0%95%EA%B8%B0%EA%B2%B0%EC%A0%9C-API-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
https://aandi.tistory.com/37
https://aandi.tistory.com/28
https://puzzle-puzzle.tistory.com/entry/import-%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C-%EB%AA%A8%EB%93%88-%EB%A7%8C%EB%93%A4%EA%B8%B0

2개의 댓글

comment-user-thumbnail
2022년 9월 6일

이런식으로 짜면 결제는 된 후에 검증이 되서 돈만 빠져나가고 DB 에 값은 입력 안되는거 아닌가요?

1개의 답글