API

스프링부트에서 페이팔 결제 API를 사용해보자 [PayPal/후기]

rexondex 2024. 10. 28. 17:58

스프링부트에서 10달러를 결제하는 페이팔 결제 버튼을 만들어 보겠습니다.

 

준비물

  • 페이팔 회원가입, 계좌가 인증된 상태여야 합니다 (2~3일 소요)
  • 페이팔 API (샌드박스 ON)
  • 스프링부트 서버 로직
  • 웹 프론트엔드 리소스 (HTML/CSS/JS)

 

페이팔 API 설정 페이지입니다 (Sandbox 활성화)

 

Sandbox모드는 API 테스트와 개발을 할 수 있도록 제공되는 테스트 환경입니다.

샌드박스 모드에서 실제 결제는 이루어지 않으며 가상의 계좌를 설정할 수 있다고 합니다.


[ 결과물 스크린샷 ]

1. 페이팔 결제 버튼 클릭

 

2. 로그인

 

3. 10달러를 결제하는 페이팔 결제 화면

 

경고문 번역 : 한국 계정끼리는 결제할 수 없습니다.

 


:: 로직 설명 및 후기 ::

<p>결제를 시작하려면 아래 버튼을 클릭하세요.</p>
<form action="/api/paypal/pay" method="post">
    <!-- 필요한 값들을 서버로 전송 -->
    <input type="hidden" name="total" value="10.00">
    <input type="hidden" name="currency" value="USD">
    <button type="submit" class="button">Pay with PayPal</button>
</form>

 

이것은 페이팔 결제 api를 호출하는 index.html의 버튼입니다.

누르면 "/api/paypal/pay" 주소를 POST 메서드로 호출합니다.

 

@RestController
@RequestMapping("/api/paypal")
public class PayPalController {

    private final PayPalService payPalService;

    @Autowired
    public PayPalController(PayPalService payPalService) {
        this.payPalService = payPalService;
    }

    @PostMapping("/pay")
    public ResponseEntity<?> pay(@RequestParam("total") Double total,
                                 @RequestParam("currency") String currency) {
        try {
            Payment payment = payPalService.createPayment(
                    total,
                    currency,
                    "paypal",  // 결제 방법
                    "sale",    // 결제 intent
                    "결제 설명",
                    "http://localhost:8080/api/paypal/cancel",  // 취소 URL
                    "http://localhost:8080/api/paypal/success"  // 성공 URL
            );

            // PayPal 승인 URL로 리다이렉트
            for (Links link : payment.getLinks()) {
                if (link.getRel().equals("approval_url")) {
                    return ResponseEntity
                            .status(HttpStatus.FOUND)
                            .header(HttpHeaders.LOCATION, link.getHref())  // 리디렉션 헤더 설정
                            .build();
                }
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("PayPal 결제 승인 URL을 찾을 수 없습니다.");
        } catch (PayPalRESTException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("결제 중 오류 발생: " + e.getMessage());
        }
    }

    @GetMapping("/success")
    public ResponseEntity<?> successPayment(@RequestParam("paymentId") String paymentId,
                                            @RequestParam("PayerID") String payerId) {
        try {
            Payment payment = payPalService.executePayment(paymentId, payerId);
            if (payment.getState().equals("approved")) {
                return ResponseEntity.ok(Map.of("status", "success", "payment", payment));
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("결제가 승인되지 않았습니다.");
        } catch (PayPalRESTException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("결제 승인 중 오류 발생: " + e.getMessage());
        }
    }

    @GetMapping("/cancel")
    public ResponseEntity<?> cancelPayment() {
        return ResponseEntity.ok(Map.of("status", "cancelled"));
    }
}

 

여기는 PayPalController 컨트롤러 클래스입니다.

 

@PostMapping("pay")에서 요청을 받고 로직을 수행할 거에요.

PayPalService 클래스를 생성자 주입 방식으로 생성하여 pay 메서드에서 payPalService.createPayment() 로 사용했어요.

 

그러면 PayPalService 클래스의 createPayment() 메서드가 어떤 메서드인지 한번 봐야겠네요

 

@Service
public class PayPalService {

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    @Value("${paypal.mode}")
    private String mode;

    // 결제 생성
    public Payment createPayment(Double total, String currency, String method, String intent, String description, String cancelUrl, String successUrl) throws PayPalRESTException {

        // 금액 설정
        Amount amount = new Amount();
        amount.setCurrency(currency);
        amount.setTotal(String.format("%.2f", total));

        // 거래 정보 설정
        Transaction transaction = new Transaction();
        transaction.setDescription(description);
        transaction.setAmount(amount);

        // 결제 방법 설정
        Payer payer = new Payer();
        payer.setPaymentMethod(method.toString());

        // 결제 생성 요청 설정
        Payment payment = new Payment();
        payment.setIntent(intent.toString());
        payment.setPayer(payer);
        payment.setTransactions(Collections.singletonList(transaction));

        // 승인 및 취소 URL 설정
        RedirectUrls redirectUrls = new RedirectUrls();
        redirectUrls.setCancelUrl(cancelUrl);
        redirectUrls.setReturnUrl(successUrl);
        payment.setRedirectUrls(redirectUrls);

        // PayPal API 호출
        APIContext apiContext = new APIContext(clientId, clientSecret, mode);
        return payment.create(apiContext);
    }

    public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException {
        // 결제 요청
        Payment payment = new Payment();
        payment.setId(paymentId);

        // 결제 실행 요청
        PaymentExecution paymentExecution = new PaymentExecution();
        paymentExecution.setPayerId(payerId);

        // PayPal API 호출
        APIContext apiContext = new APIContext(clientId, clientSecret, mode);
        return payment.execute(apiContext, paymentExecution);
    }

}

 

클라이언트 아이디(API키)는 @Value("${example}")로 application.properties에 기재된 값을 가져와 사용합니다.

createPayment()는 7개의 파라미터를 전달받아 PayPal API를 호출하는 메서드네요.

 

한국에서 사용불가

 

샌드박스 모드여도 한국 계정간 결제할 수 없다는 창 때문에 결제 진행이 불가능했습니다.

원래대로 진행되었다면 PayPal 결제 승인 시 "/api/paypal/success" 로 리디렉션 됩니다.

@GetMapping("/success")
public ResponseEntity<?> successPayment(@RequestParam("paymentId") String paymentId,
                                        @RequestParam("PayerID") String payerId)

 

그러면 컨트롤러 클래스가 URL을 받아 successPayment 메서드에서 결제 성공 여부를 검증하고 결과를 반환합니다.

 

단순히 PayPal이 결제 승인했다고 바로 결과를 반환하지 않고, 서버에 저장된 데이터와 응답 결과가 일치하는지 한번 더 검증하는 절차를 거치게 됩니다.

이렇게 하면 결제 관련인만큼 절차는 복잡하지만 더 안전하게 처리할 수 있을 거에요.

 

그리고 마지막, 성공 시 응답으로 success.html을 반환하며 내용은 이렇습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>결제 성공</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 50px;
            text-align: center;
        }
        h1 {
            color: green;
        }
    </style>
</head>
<body>
<h1>결제가 성공적으로 완료되었습니다!</h1>
<p>감사합니다. 결제가 완료되었습니다.</p>
</body>
</html>

 

결제가 취소된 경우 cancel.html을 반환합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>결제 취소</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 50px;
            text-align: center;
        }
        h1 {
            color: red;
        }
    </style>
</head>
<body>
<h1>결제가 취소되었습니다.</h1>
<p>결제가 취소되었습니다. 다시 시도해주세요.</p>
</body>
</html>

[ 후기 ]

이렇게 10달러를 결제하는 페이팔 API를 호출하는 버튼을 만들어 보았습니다.

페이팔 자체는 한국에서 편리하게 사용할 수 있는 시스템은 아니라고 생각됩니다.

 

처음 결제 API 구현을 시도할 때는 글로벌적인 AWS 시스템을 사용해보는 김에 페이팔을 선택했었습니다.

지금 결제 API 구현을 시도한다면 아마 카카오페이를 가장 우선순위로 API를 구현 및 테스트 할 것입니다.

그리고 수수료는 높지만 카드사 통합 결제를 지원하는 "포트원" 이라는 국내 API도 있습니다. 여러 결제방법 중 요구사항에 적합하게 선택하면 될 것 같습니다.

 

결제 API에서 핵심은 단순히 외부 결제 API의 응답을 그대로 유저에게 반환하는 것이 아니라,

페이팔 API가 응답한 메시지 (성공/실패)를 바탕으로 페이팔에게 전달했던 파라미터와 현재 서버에 저장된 결제 정보를 대조하여 오류가 없는지 검증합니다.

여기서 결제정보의 무결성이 확인되면, 그때서야 유저에게 결제 결과를 반환합니다.

 

모든 정보가 정상적으로 전달되면 문제가 없겠지만, 데이터 누락이나 통신 오류가 발생할 수도 있기 때문에 검증 과정도 거치도록 로직을 작성합니다.

 

결제를 수행하는 API 로직을 이해하는 데 조금 더 많은 노력이 들었습니다.

데이터를 대조하고 검증하는 과정이 결제 API에서는 꼭 필요하다는 것을 알 수 있었습니다.