본문으로 바로가기

Stripe 결제 시스템 도입하기

category Coding/설계 | 경험 2020. 12. 10. 10:37
반응형

1. 개요

기존 해외결제의 경우 아임포트를 통해 페이팔 결제 시스템을 사용하고 있었다. 사용상의 불편함은 없었지만 데이터 분석 결과 나름 치명적인 문제가 하나 발견되었는데, 바로 결제용 팝업 창이 새롭게 뜨는 문제였다. 이게 왜 문제가 되는 걸까? 데이터 분석팀을 통해 유저 행동을 분석해보니 결제창이 뜬 이후 이탈하는 빈도가 굉장히 높은 모습을 찾을 수 있었고, 이 또한 가설이지만 모달/팝업 없이 같은 페이지에서 카드 번호등을 입력하여 결제하는 방식을 제공한다면 보다 높은 결제를 유도할 수 있을 것이라 판단했다. 따라서 결제를 위한 모달 창이나 팝업이 뜨는것이 아닌 임베디드 형태로 페이지내부에 삽입할 수 있는 방법을 찾아보기 시작하였다. 

그러다 찾아낸 서비스가 바로 Stripe이다. 이미 해외에서는 유명한 서비스이며 널리 사용되고 있는 서비스였다. 물론 Stripe도 팝업같은 형태로 새로운 창이 뜨는 기능 또한 제공하지만 궁극적으로 우리에게 꼭 필요한 임베디드 형식도 제공해주기 때문에 현재로써는 최선의 선택지라고 생각되었다. 추가적으로 간단한 결제 플로우와 친절한 API문서까지 제공되기때문에 처음 접하는 개발자도 보다 손쉽게 서비스에 적용이 가능할 것으로 보였다.

 

1-1. 종류

1. Stripe Checkout: Pre-built

2. Charges API: User Custom(Deprecated soon)

3. Payment Intents API: User Custom

 

개요에서 설명했던 것 처럼 Stripe는 위처럼 총 3가지의 방법을 제공한다. 1번의 경우 기존처럼 팝업/모달이 뜨는 형태이며 기본적으로 스타일링이 되어있기 때문에 프론트단에서 별다른 추가작업없이 결제를 진행할 수 있다. 하지만 우리는 임베디드 형식을 원하므로 1번은 제외했다. 2번의 경우 곧 Deprecated 될 예정이라고 나와있었기 때문에 결과적으로 3번인 Payment Intent 방식을 사용하기로 했다. 프론트단은 User Custom하게 구현할 수 있으며 우리에게 필요한 임베디드 형태를 제공해주기 때문이다.

 

2. Flow

먼저 클라이언트는 서버에 Client Secret을 요청한다. 서버는 비밀 키를 통해 Client Secret을 클라이언트에게 발급해준다. 클라이언트는 발급받은 Client Secret과 카드 정보등을 Stripe 서버로 전송하면 결제가 완료된다. 정말 간단하지 않은가? (참고로 결제 금액은 최초 Client Secret을 요청할 때 서버에서 결정된다)

여기서 한가지 더 알아둘점이 있는데, 최초 클라이언트에서 서버로 Client Secret을 요청할 때 Payment Intent가 Incomplete상태로 시작된다. (Stripe 대시보드에서 확인 가능) 또한 Client Secret을 제대로 발급받지 못한다면, 프론트단에서 카드 정보를 입력하는 인풋 창 또한 활성화되지 않는다. 그리고 발급받은 Client Secret과 카드 정보를 통해 결제를 완료하면 최초 생성된 Payment Intent가 Complete 상태로 변경되며 결제가 완료된다.

여기서 결제에 대한 정보를 데이터베이스와 연동하길 원한다면 추가적인 플로우가 요구되는데 이를 그림으로 나타내자면 다음과 같다.

 

1, 2번은 앞서 설명한 내용과 동일하다. Stripe까지 결제를 완료했다면 완료 응답에 포함되어있는 pi_ 로 시작하는 Payment Intent 고유 키값을 서버로 전송하여 결제 완료를 요청한다. 서버는 전달받은 값을 통해 Stripe에 정보를 요청하여 실제로 결제가 완료된 상태인지 파악하고 맞다면 내부 데이터베이스 결제건도 성공으로 처리한다.

여기서 한가지 의문점이 생길수도 있다. 2번 Client Secret을 통해 Stripe 서버로 결제 요청하는 부분 또한 서버에서 직접 처리할 수 있지 않나요? 라는. 물론 그또한 맞는 말이다. Stripe API또한 그러한 기능을 제공해주고 있으며 구현이 불가능한건 아니다. 하지만 굳이 서버에서 처리할 필요가 없다는 점, 프론트에서 Stripe 서버로 직접 요청하여 처리하는 것과 서버로 요청하여 처리하는 것의 차이가 크지 않기에 한번의 Request라도 절약시킬 수 있는 점으로 인해 위처럼 흐름을 구성했다. 위 내용을 글로써 한번 더 풀어써보자면 아래와 같다.

 

1. 클라이언트에서 서버로 client secret을 요청한다. 이 때 서버에서는 Payment Intent를 시작하고 Incomplete상태로 생성한다.

2. 클라이언트는 서버로부터 전달받은 client secret을 통해 Stripe 서버에 결제 완료 요청을 보낸다. 이 때 1번에서 Incomplete였던 Payment Intent가 Succeeded로 변경된다.

3. 클라이언트는 2번에 대한 응답으로 status가 succeeded인지 확인함을 통해 결제가 성공적으로 완료됐는지 확인할 수 있다. 이제 서버에도 결제 완료 처리를 해줘야 한다. 

4. 3번에서 받은 응답 중 id키값을 확인해보면 pi_로 시작하는 고유 식별값이 있다. 해당 값을 서버로 전송한다.

5. 서버에서는 전달받은 pi_ 고유 식별값을 Stripe 서버로 보내 실제로 결제 완료 상태인 지(paid == true) 확인한 후 처리한다.

 

3. 주요 API

몇가지의 주요 API에 대해 살펴보자. 우리 서버는 파이썬을 사용하고 있으므로 stripe라이브러리를 설치해준 상태이며 해당 라이브러리를 이용하여 예제를 작성했다.

 

stripe.PaymentIntent.retrieve(pi_id)

PaymentIntent의 정보를 추출하는 메소드이다. 현재 Payment Intent가 완료된 상태인 지 알 수 있으며 카드 정보 등 주요 정보들을 가져온다.

 

stripe.PaymentIntent.modify(pi_id, **kwargs)

Payment Intent의 정보를 수정하는 메소드이다. Payment Intent는 내부에 metadata라는 필드를 가지고 있고 해당 필드를 수정할 수 있기 때문에 일반적으로 주문자의 정보, 결제 정보등을 metadata에 저장시킨다.

 

stripe.PaymentIntent.create(amount=amount, currency=currency)

Client Secret의 경우 Payment Intent가 시작됨과 동시에 발급된다. 따라서 위 요청의 응답에 Client Secret이 포함되어있고 해당 값을 클라이언트에게 전달해줘야 한다. 생성 당시 amount, currency등을 설정함으로써 서버측에서 가격 정보를 결정한다.

 

stripe.Refund.create(charge=ch_id)

환불을 해주는 메소드이다. 환불의 경우 Payment Intent의 고유 값인 pi_~ 가 아니라 Charge에 해당하는 ch_~ 값을 통해 진행한다. 이는 Payment Intent와 Charge는 다른 개념이기 때문인데, ch_~ 값의 경우 위에서 설명한 PaymentIntent.retrieve() 를 통해 확인할 수 있다.

 

기타 API들은 https://stripe.com/docs/api 를 통해 확인해보자. 문서화가 상당히 깔끔하게 되어 있어 읽는데 전혀 불편함이 없을 것이다.

 

4. 영수증 전송

stripe.PaymentIntent.modify(pi_id, receipt_email="abc@abc.com" )

영수증의 경우에도 Stripe측에서 상당히 간단한 방법으로 제공해주고 있는데, 3번 주요 API에서 설명한 PaymentIntent.modify()를 이용하면 된다. 해당 메소드의 인자로 receipt_email값을 주게 된다면 영수증을 보낼 메일로 업데이트가 됨과 동시에 영수증을 발행해준다.

영수증은 위와 같은 형태로 전송되는데 주의할점은 Payment Intent의 Description부분이 영수증의 SUMMARY, 제목으로 나간다는 점이다. 최초 개발시에는 디버깅에 용이하게 하기 위해 내부적으로 식별할 수 있는 키 값들을 Description에 저장시켜놨었는데, 영수증에 나가는걸 보고 급하게 수정했던 경험이 있다. 따라서 아래와 같이 원하는 제목으로 반드시 수정해줘야 한다.

stripe.PaymentIntent.modify(pi_id, description="Wanted PLUS" )

(참고로 말하자면, 영수증은 테스트 모드일때는 전송되지 않는다)

 

5. Callback

콜백은 현재 많은 부분에 걸쳐 사용하고 있지는 않고 민감한 부분인 환불 건에 대해서만 사용하고 있다. 먼저 콜백은 Stripe 대시보드로 들어가서 왼쪽 메뉴 중 Developers - Webhooks 메뉴에 들어가면 된다. 해당 메뉴에서 Add endpoint를 누르면

 

콜백을 받을 엔드포인트를 설정할 수 있고 Events to send부분을 통해 어떠한 이벤트에 대해 콜백을 보낼 지 설정할 수 있다. 정말 많은 기능에 대해 콜백을 제공하고 있는데 우리는 환불에 대한 콜백을 설정할 것이므로 charge.refunded 이벤트를 선택했다. Stripe 콜백은 Reply attack등을 방지하기 위해 상당히 Nice한 방법을 제공하는데, 모든 콜백에 대해 Verify용 Signature를 같이 전송한다는 점이다. 

먼저 콜백 데이터는 POST Method, HTTP Body로 전송되어온다. 또한 Signature는 Stripe-Signature라는 이름으로 HTTP Header에 포함되어온다.

 

stripe.Webhook.construct_event(
body, sig_header, refund_signing_secret,
)

(refund signing secret의 경우 콜백 설정 페이지를 보면 Signing Secret쪽에 나와있다)

 

위 API요청을 보내면 Signature를 검증하고 실패했다면 SignatureVerificationError 오류를, 성공했다면 콜백의 내용을 응답으로 보내준다. 해당 값을 통해 원하는 작업을 수행하면 된다.

 

6. 주의 사항

- 가격은 무조건 정수로 입력해야 한다. (실수 입력 불가)

- Zero decimal currency에 해당하는 화폐가 아니라면 가격 * 100을 해줘야 한다. https://stripe.com/docs/currencies#zero-decimal

- 화폐별로 지원하는 최소 금액이 정해져있다. 예를 들어 USD인 경우 0.5가 최소 단위이다. https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts

 

7. 후기

좋았던 점

- 타 서비스들과는 다르게 임베디드 형태를 제공하기 때문에 원하는 디자인으로 커스터마이징하기에 용이하다.

- 구현이 정말 간단하며 API문서화가 수준급이다.

- 대시보드에서 Client Secret 생성 - 결제 성공 - 환불 등의 여러가지 요청에 대해 시간 순으로 정렬해서 보내준다. 또한 각 케이스별로 자세한 상태(pi_id, ch_id 등)를 보여주기 때문에 디버깅이 굉장히 편했다.

 

아쉬웠던 점

- 한국 계좌에 대한 지원을 하지 않는다.

반응형