개요

Spring 프로젝트를 진행하면서 결제 구현이 필요했다.

image

실제 PG 사와 카드사를 거쳐 구현하기에는 까다로운 요소들이 많았고, 이를 간단하게 구현할 수 있는 PortOne API 를 사용하기로 하였다.

해당 게시물은 Spring Boot 만을 사용하였다.

코드

1. Front

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Payment Page</title>
    <!-- jQuery CDN 추가 -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!-- 아임포트 스크립트 추가 -->
    <script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
</head>
<body>
<!-- 버튼들 -->
<button id="cardPay" onclick="handlePayment('html5_inicis.INIpayTest', 'card')">카드 결제</button>
<button id="kakaoPay" onclick="handlePayment('kakaopay', 'card')">카카오페이 결제</button>

<script th:inline="javascript">
    // 아임포트 코드
    var impCode = /*[[${@environment.getProperty('imp.code')}]]*/ '';

    function handlePayment(pg, payMethod) {
        console.log("handlePayment");
        console.log(pg);
        console.log(payMethod);

        var order = {
            productId: 1,
            productName: '상품1',
            price: 3000,
            quantity: 1
        };

        // 결제하기 버튼 클릭 시 결제 요청
        IMP.init(impCode);
        IMP.request_pay({
            pg: pg,
            pay_method: payMethod,
            merchant_uid: '212R3A11TD233AAC', // 주문번호 생성
            name: '상품1',
            amount: 3000, // 결제 가격
            buyer_name: '김민규',
            buyer_tel: '010-1234-5678'
        }, function(rsp) {
            if (rsp.success) {
                // 결제 성공 시
                $.ajax({
                    type: 'POST',
                    url: 'api/v1/payment/validation/' + rsp.imp_uid
                }).done(function(data) {
                    console.log(data);
                    if (order.price == data.response.amount) {
                        order.impUid = rsp.imp_uid;
                        order.merchantUid = rsp.merchant_uid;
                        // 결제 금액 일치. 결제 성공 처리
                        $.ajax({
                            url: "api/v1/payment/order",
                            method: "post",
                            data: JSON.stringify(order),
                            contentType: "application/json"
                        }).then(function(res) {
                            console.log("res", res);
                            console.log("rsp", rsp);
                            var msg = '결제가 완료되었습니다.';
                            msg += '고유ID : ' + rsp.imp_uid;
                            msg += '상점 거래ID : ' + rsp.merchant_uid;
                            msg += '결제 금액 : ' + rsp.paid_amount;
                            msg += '카드 승인번호 : ' + rsp.apply_num;
                            alert(msg);
                        }).catch(function(error) {
                            alert("주문정보 저장을 실패 했습니다.");
                        });
                    }
                }).catch(function(error) {
                    alert('결제에 실패하였습니다. ' + rsp.error_msg);
                });
            } else {
                alert(rsp.error_msg);
            }
        });
    }
</script>


</body>
</html>

결제는 클라이언트가 직접 portone 에 결제 요청하면서 시작된다.

타임리프로 구성된 페이지를 자세히 살펴보면 총 3가지 단계로 구성되어있다.

  1. 포트원 라이브러리를 추가하고 객체를 초기화한다.
  2. 서버에서 실제 결제 건을 조회한다.
  3. 결제 금액이 일치한다면 Order 객체를 서버에 저장한다.

클라이언트 측에서 라이브러리를 다운받기만 하더라도 다음과 같은 결제창을 얻을 수 있다.

image

image

2. Controller

@RestController
@RequestMapping("/api/v1/payment")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "Payments", description = "결제 API")
public class PaymentController {
    private final PaymentService paymentService;

    @PostMapping("/validation/{imp_uid}")
    public IamportResponse<Payment> validateIamport(@PathVariable String imp_uid) throws IamportResponseException, IOException {
        log.info("imp_uid: {}", imp_uid);
        log.info("validateIamport");
        return paymentService.validateIamport(imp_uid);
    }

    @PostMapping("/order")
    public ResponseEntity<String> processOrder(@RequestBody OrderDto orderDto) {
        // 주문 정보를 로그에 출력
        log.info("Received orders: {}", orderDto.toString());
        // 성공적으로 받아들였다는 응답 반환
        return ResponseEntity.ok(paymentService.saveOrder(orderDto));
    }

    @PostMapping("/cancel/{imp_uid}")
    public IamportResponse<Payment> cancelPayment(@PathVariable String imp_uid) throws IamportResponseException, IOException {
        return paymentService.cancelPayment(imp_uid);
    }
}

3.Service

@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {
    private final IamportClient iamportClient;
    private final OrderRepository orderRepository;

    /**
     * 아임포트 서버로부터 결제 정보를 검증
     * @param imp_uid
     */
    public IamportResponse<Payment> validateIamport(String imp_uid) {
        try {
            IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);
            log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse());
            return payment;
        } catch (Exception e) {
            log.info(e.getMessage());
            return null;
        }
    }

    /**
     * 아임포트 서버로부터 결제 취소 요청
     *
     * @param imp_uid
     * @return
     */
    public IamportResponse<Payment> cancelPayment(String imp_uid) {
        try {
            CancelData cancelData = new CancelData(imp_uid, true);
            IamportResponse<Payment> payment = iamportClient.cancelPaymentByImpUid(cancelData);
            return payment;
        } catch (Exception e) {
            log.info(e.getMessage());
            return null;
        }
    }

    /**
     * 주문 정보 저장
     * @param orderDto
     * @return
     */
    public String saveOrder(OrderDto orderDto){
        try {
            orderRepository.save(orderDto.toEntity());
            return "주문 정보가 성공적으로 저장되었습니다.";
        } catch (Exception e) {
            log.info(e.getMessage());
            cancelPayment(orderDto.getImpUid());
            return "주문 정보 저장에 실패했습니다.";
        }
    }
}

우선 Config 를 통해 IamportClient 를 Bean 으로 등록한다.

이전에는 token 을 발급받고 특정 url 을 통해 결제 조회 및 취소를 진행했지만 지금은 IamportClient 를 통해 간편하게 조회 및 취소가 가능하다.

image

추가

PG 테스트 설정

image

해당 에러가 발생했었다. 이는 PortOne 에서 PG 설정을 등록하지 않았기 때문인다.

image

테스트를 위해서 혹은 실제 결제를 위해 등록 과정이 필요하다.

image

참고

Github Code

PortOne 개발자 센터

업데이트:

댓글남기기