티스토리 뷰

 

Billing 연동

  • 토스 페이먼츠를 사용하여 카드 자동결제 (Billing) 연동을 해보겠습니다.
  • 본 글에서는 카드정보를 입력 받아 정보에 해당하는 Billing Key를 발급 받는 과정까지를 진행해봅니다.

Billing 이란?

  • 최근 구독 형태의 서비스를 하는 서비스들은 유저에게 카드 정보를 입력을 받아 정기적으로 (Monthly) 결제를 수행하는 방식을 이용하고 있습니다. 
  • 보통 일반 P.G의 결제 기능을 사용하게 되면 매번 결제 시점에 유저가 인증을 해야 하는 방식입니다. 
  • 이때 사용할 수 있는 방식 중 하나가 P.G 사에서 제공하는 정기결제(Billing) 기능을 사용하여 구현할 수 있습니다.
    • 카드 등록 시점 이후에는 P.G 사에서 제공하는 Billing Key 를 통해서 결제 요청을 할 수 있는 방식

Toss Payments

  • 토스 페이먼츠에서 제공하는 Billing 서비스의 연동 과정은 아래와 같습니다. 
    • 실제 연동 문서는 여기 에서 확인

  • 여기서 눈여겨 볼 점들은 결제창 구현 방식 입니다.

결제창

  • 실제 유저에게 카드 정보를 입력받는 결제 창으로 보통 두가지 방식으로 구현할 수 있습니다.
  • 각 방법에는 장단점이 존재합니다.
    • 참고로 이번 예제에서는 P.G 결제창을 연동합니다.

P.G 제공 결제창

  • P.G 사에서 제공하는 결제창을 그대로 사용 (Javascript 등 제공되는 SDK 사용)
  • P.G사 결제창을 사용하는 경우에는 P.G사에서 제공하는 기능을 그대로 사용하면 되기 때문에 비교적 연동이 쉽다는 장점이 있지만 P.G사 결제창의 구현 방식에 의존적이라는 단점이 있습니다.
    • 토스 페이먼츠의 경우 Iframe 방식으로 결제창을 로드하는데 만약 Product 의 구현 방식에 따라 (iframe 이 아닌 popup으로 띄워야 된다거나, 결제 과정중 추가적인 처리가 필요하다거나) 제약 사항이 생길 수 있습니다.
    • 토스 페이먼츠의 경우 본인인증을 위해서 '휴대폰 인증'을 수행하는데 이 또한 유저의 이탈을 발생시킬 수 있기 때문에 고려해볼 점이긴 합니다.

Custom 결제창

  • P.G 사에서 제공하는 API를 연동하여 직접 구현한 Custom 결제창 사용
  • 결제창을 직접 구현하는 경우 결제창 자체를 구현하는데 추가적인 리소스가 들지만 직접 구현하는 만큼 구현방식에 대한 자유도를 얻을 수 있습니다. (ex. 토스 결제창의 약관 동의 Step을 생략한다던지 .. )
    • 토스 페이먼츠 연동 문서에는 API 연동 방식에 대한 내용이 없는 것으로 보여 직접 문의를 해봐야 합니다.

설계

Sequence Diagram

  • 아래는 추후 구현할 Application의 Flow의 시퀀스 다이어그램 형태입니다. 
    • 위 Toss Payments의 Flow를 실제 구현 스펙에 맞게 구체적으로 옮겨본 것으로 보시면 됩니다.

  • 각 단계별로 자세히 보겠습니다.

 

  1. 카드 등록 (버튼 클릭)
    • 유저가 카드 등록을 하는 순간 (예제에서는 간단한 버튼클릭) JavaScript 코드를 통해서 Toss 결제창 라이브러리를 호출
    • 이때 Toss Client Key, Success/Fail Redirect URL 을 설정
  2. 결제창 호출
    • Toss 라이브러리를 통해서 결제창이 호출되고 Iframe 형태로 결제창이 Display
  3. 카드 정보 입력
    • 유저가 카드 정보 입력을 완료
  4. 카드 입력 결과 redirect (SUCCESS/FAIL)
    • 결제창으로 입력된 카드 정보가 유효한지를 판단한 뒤 1번에서 설정한 Success/Fail URL로 redirect
  5. SUCCESS/FAIL 처리
    1. (SUCCESS인 경우) 빌링키 발급 요청
      • 성공인 경우 빌링키 발급을 위한 authKey와 요청에 대한 식별자인 customerKey가 전달
      • 해당 정보를 포함해서 Toss API 로 빌링키 발급 요청을 보냄
    2. (FAIL인 경우) 실패 화면 응답
      • 실패인 경우 (유효하지 않은 카드 정보이거나) 실패 코드, 실패 사유가 전달된다.
      • 예제에서는 실패 코드, 사유를 담아 실패 View를 리턴
  6. 빌링키 응답
    • 성공인 경우 빌링키를 응답받음. (실패인 경우 5-1 과 동일)
  7. 성공 화면 응답
    • 발급된 빌링키를 담아 성공 View를 내려준다. 

 


구현

  • 위 설계를 토대로 실제 구현을 해보겠습니다. 
  • 구현 환경은 아래와 같습니다. 
    • Back-end: Kotlin Spring Boot 
    • Front-end: Thymleaf (편의를 위해서 Template Engine 을 사용)

소스코드

  • 예제 전체 소스코드는 아래 위치에서 확인해보실 수 있습니다.
 

GitHub - rlawlstjd0077/TIL: Today I Learned

Today I Learned. Contribute to rlawlstjd0077/TIL development by creating an account on GitHub.

github.com


클라이언트, 시크릿 키 확인 방법

  • 이후 예제에서 쓰일 클라이언트 키, 시크릿 키를 확인할 수 있는 방법입니다. 
    • 토스 페이먼츠 로그인 -> 개발 연동 -> 테스트 탭에서 키 값 확인


메인 화면

  • 버튼 하나 존재하는 아주 심플한 HTML 코드입니다.
  • 버튼 클릭시에 tossPayment 라이브러리를 호출하여 결제창을 Display 합니다. (Success/Fail URL 설정)
    • 설정 값에 대한 자세한 내용은 이 곳 에서 확인 
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>토스 페이먼츠 결제 연동 예제</title>
    <script src="https://js.tosspayments.com/v1"></script>
</head>
<body>
<button id="card_button">카드 등록</button>

<script type="text/javascript" th:inline="javascript">
    window.addEventListener('load', (event) => {
        document.getElementById("card_button").addEventListener("click", function() {
            var clientKey = "발급받은 클라이언트 키 (위에서 확인 방법 확인)";
            var tossPayments = TossPayments(clientKey);
            tossPayments.requestBillingAuth("카드", {
                customerKey: "customer_key",
                successUrl: window.location.origin + "/success",
                failUrl: window.location.origin + "/fail"
            });
        });
    });
</script>
</body>
</html>

Controller 

  • 카드 정보 입력 후 결제창으로 부터 redirect 되는 API 요청을 처리하기 위한 Controller 를 구현해보겠습니다. 
  • 각각 주석에 대해서 자세하게 살펴보겠습니다. 
@Controller
class BillingController {
    private val objectMapper = ObjectMapper()

    @RequestMapping("/success")
    fun success(
        /**
         * 1. Request Parameter 정보
         *
         * - authKey: 빌링키를 얻을 때 사용하는 인증 키
         * - customerKey: 가맹점에서 사용하는 사용자별 고유 ID (내부에서 사용하는 카드 소유주별 식별자)
         */
        @RequestParam requestParams: Map<String, String>,
        model: Model,
    ): String {
        /**
         * 2. 발급 받은 시크릿 키 Base64 인코딩
         */
        val encodedAuthHeader = Base64.getEncoder().encodeToString(("$SECRET_KEY:").toByteArray())

        /**
         * 3. 빌링키 발급 Reequest 생성
         */
        val request: HttpRequest = HttpRequest.newBuilder()
            .uri(URI.create("https://api.tosspayments.com/v1/billing/authorizations/${requestParams.getValue("authKey")}"))
            .header("Authorization", "Basic $encodedAuthHeader")
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString("{\"customerKey\":\"${requestParams.getValue("customerKey")}\"}"))
            .build()

        val response: HttpResponse<String> =
            HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())

        return if (response.statusCode() == OK.value()) {
            /**
             * 4. 빌링키, 카드 정보 포함된 Json 으로 성공 View 리턴
             */
            val jsonNode = objectMapper.readTree(response.body())
            model.addAttribute("response", jsonNode.toPrettyString())
            "success"
        } else {
            /**
             * 5. 실패 View 리턴
             */
            model.addAttribute("message", "카드 정보를 저장하는데 실패하였습니다.")
            "fail"
        }
    }

    @RequestMapping(value = ["/fail"])
    fun billingFail(
        /**
         * 1. Request Parameter 정보
         *
         * - code: 실패 코드
         * - customerKey: 실패 사유
         */
        @RequestParam(required = false) code: String?,
        @RequestParam(required = false) message: String?,
        model: Model,
    ): String {
        model.addAttribute("code", code)
        model.addAttribute("message", message)
        return "fail";
    }

    companion object {
        const val SECRET_KEY = "발급 받은 시크릿 키 (발급 방법 위에서 확인)"
    }
}

 

  1. Autentication 성공 Request Param 정보 
    • 카드 인증이 성공하여 redirect 되는 경우 authKey, customerKey 가 파라미터로 전달된다.
      • 자세한 내용은 이 곳을 참고
    • customerKey는 해당 카드에 대해서 내부에서 관리하는 식별자 정보로 인증의 용도로 사용된다고 이해하면 됨
    • 보안 강화를 위해서 일회성 토큰을 사용해서 인증 단계를 추가로 진행하기도 하는 데 이건 나중에 기회가 되면 .. 다뤄보겠습니다. 
  2. 시크릿 키 인코딩
    • Toss Payments 에서는 Basic Authorization 인증방식을 사용해서 발급 받은 시크릿키를 시크릿키: 형식으로 Base64 인코딩 하여 요청 헤더에 넣어줘야 합니다. 
    • 인증에 대한 자세한 내용은 이 곳을 참고 
  3. 빌링키 발급 Request 생성
    • 빌링키 요청 포맷에 맞춰 authKey, customerKey 를 포함하여 요청 객체를 생성합니다.
    • 빌링키 발급 요청에 대한 내용은 이 곳을 참고
  4. 빌링키 발급 성공 처리 
    • 발급에 성공한 경우 빌링키, 카드 정보를 포함한 정보가 리턴됩니다. 
    • 자세한 응답 값에 대한 내용은 이 곳참고
  5. 빌링키 발급 실패 처리 
  6. Autentication 실패 Request Param 정보
    • 카드 인증이 실패한 경우 실패 코드, 사유 정보와 함께 redirect 됩니다.
    • 실패에 대한 스펙은 연동 문서에 없는 것 같아 보이는 군요 .. 흠.

 


Success/Fail 

  • 자, 이제 마지막 단계입니다.
  • 성공, 실패에 대한 View 파일인데 이 또한 아주 심플한 모습입니다.

success.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>등록 성공 안내</title>
</head>
<body>
    <h1>카드 등록에 성공하였습니다.</h1>
    <div>
        <label>카드 정보: </label>
        <label th:text="${response}"/>
    </div>
</body>
</html>

fail.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>등록 실패 안내</title>
</head>
<body>
    <h1>카드 등록에 실패하였습나다.</h1>
    <div>
        <label>실패 코드: </label>
        <label th:text="${code}"/>
    </div>
    <div>
        <label>실패 사유: </label>
        <label th:text="${message}"/>
    </div>
</body>
</html>

 

 

  • 본 글의 소스코드는 Toss Payment 결제창 연동, 빌링키 API 연동 방식에 대한 Core 소스를 이해하는데 초점을 맞추고 있어 실제 실행을 위해서는 아래 프로젝트 전체 소스코드로 실행을 해보는 걸 권장합니다.  
 

GitHub - rlawlstjd0077/TIL: Today I Learned

Today I Learned. Contribute to rlawlstjd0077/TIL development by creating an account on GitHub.

github.com


Demo


Next...

  • 다음 글에서는 발급 받은 빌링키를 가지고 실제 결제 요청을 하는 예제를 진행해보겠습니다.

 

 

 

 

 

 

 

반응형
댓글