2023. 11. 17. 09:08ㆍ기술적 이슈 정리
이 게시글은 payment-lab이라고 하는 결제모듈 연동 사이드프로젝트를 진행하던 중 발생한 기술적 이슈를 해결하기 위해 정리하는 글입니다.
결제 모듈을 개발하던 중 한가지 고민에 빠졌습니다.. 바로 사용자가 실수로 중복 결제(일명 따닥이)를 하게될 경우 어떻게 대비할지에 대한 고민이죠. 현재 진행하는 프로젝트는 토스페이먼츠로 결제 연동을 수행하고 있습니다. 토스페이먼츠의 경우 이러한 중복 결제 혹은 취소를 방지하기 위해 멱등키를 제공하고 있습니다.
이러한 멱등키를 적용하는 것은 너무나도 당연합니다. 그러나 그 멱등키를 어떻게 생성해야할까요? 만약 그냥 결제 요청마다 무작위의 uuid로 적용한다면 무슨일이 일어날까요?
사실상 멱등키를 적용하나마나일겁니다. 어차피 결제 요청 한번 할때마다 유일한 멱등키를 제공할테니, 사용자가 실수로 n번을 동일한 주문을 결제 요청 한다면, n-1번 만큼의 중복 결제가 발생하는건 마찬가지일겁니다.
따라서 결제 중복을 막기위해 멱등키를 적용하는 것 이전에 멱등키를 어떻게 생성하느냐에 대해 먼저 생각해볼 필요가 있습니다. 이 방법은 너무나 다양할 수 있기에 한번에 적절한 수단을 찾는건 어려워 보입니다. 그러니 먼저 결제 중복을 막는 브레인스토밍을 통해 여러 방법을 찾아보고 그 방법이 왜 안되는지를 설명해보며 하나씩 힌트를 얻어보겠습니다.
case1: 프론트엔드 ui로 결제 중복 방지하기
즉, 결제 하기 버튼을 한 번 누르면 해당 버튼을 비활성화 시켜서 결제가 완료될때까지 사용자가 누르지 못하게 하는 겁니다. 얼핏보면, 매우 좋은 방법인듯 합니다. ui를 비활성화 시키는 것은 매우 쉽습니다. 또한 한번 누르는 순간 ui 이벤트가 발생하는걸 원천 봉쇄하니 굳이 멱등키를 적용할 필요도 없어보입니다.
그러나.. 프론트엔드의 ui는 자바스크립트로 되어있습니다. 크롬의 개발자 도구와 같은 툴로 자바스크립트를 인위적으로 봉쇄할 수도 있고, 만약에 몇몇 사용자가 브라우저의 설정을 통해 자바스크립트를 아예 사용하지 않는다면, 자바스크립트로 아무리 잘 ui를 비활성화 시킨다할지라도 사용자가 실수로 결제 버튼을 n번을 누르면 n-1 만큼의 중복 결제가 승인되는건 마찬가지 입니다.
case2: 결제 승인 요청 내용 조합 후 Base64 인코딩 적용
이 방법도 단순합니다. 그러나 이 방법은 다른 문제점이 있습니다. 바로 '결제 누락'이죠. 만약 사용자가 같은 물건을 다시 결제하고 싶은데, 멱등키를 기존 결제 승인 요청 내용을 토대로 지정한다면, 결제 처리가 진행이 안될겁니다.
따라서, 이 방법도 그리 좋은 방법은 아닌 듯 합니다.
중간 점검: 사용자가 결제를 확정짓는 시점을 알아보자
두 가지의 케이스로 가정을 해보았습니다. case1의 경우는 일단은 절대 해서는 안될 것 같습니다. 그러나 case2의 경우는 어떨까요? 정답은 아닐지라도 어느정도는 수용할만하다고 봅니다. 다만 결제 요청에 속한 상품들을 기준으로 삼지만 않는다면 말이죠.
따라서 적절한 기준을 정하면 될 것 같은데, 스스로 힌트를 얻기 위해 사용자가 커머스 도메인을 통해 물건을 구매하는 과정을 살펴보겠습니다.
사용자가 물건을 구매하는 프로세스
- 사고싶은 상품을 고른다. -> 장바구니에 담는다. -> 장바구니 페이지에 방문하여 확인 후 구매하기 버튼 클릭 -> 주문/결제 정보 입력, 확인 후 '결제하기' 클릭
- 사고 싶은 상품을 고른다. -> 바로 구매하기 버튼 클릭 -> 주문/결제 정보 입력 확인 후 '결제하기' 클릭
사용자가 물건을 구매하는 프로세스는 이렇게 두 가지가 될 것 같은데, 이 두 가지의 공통점은 '주문/결제 정보 입력'입니다. 현재 사이드프로젝트에서는 주문 도메인을 다루고 있진 않지만, 만약 사용자의 주문의 중복여부를 결정할 수 있는 요소를 고르라고 하면 이 결제가 완료된 후 생성되는 '주문'일 것입니다.
따라서, 결제 이후에 생성될 주문의 고유번호를 생성하는 시점의 정보를 조합하여 멱등키로 지정하면 될 것 같습니다. 그렇다면.. 이제 이 시점에서 생성되는 어떤 정보를 멱등키로 정하면 될까요?
case3: 결제/주문 페이지의 세션ID 활용
아시다시피 사용자들이 방문하는 각각의 웹 페이지에는 고유한 세션ID가 있습니다. 요즘엔 거의 JWT를 활용하는 추세지만, 이 세션ID를 통해 사용자 인증 기능을 구현하기도 하죠.
따라서, 주문/결제 페이지의 세션ID를 서버가 기억하고 있고, 그 ID를 멱등키로 활용한다면, 사용자가 실수로 결제하기 버튼을 N번 클릭을 해도 N-1 만큼의 결제 승인 요청의 멱등키가 기존 주문/결제 페이지의 세션ID일테니 당연히 최초 1회 이후로는 결제가 확정되지 않을 것입니다. 그런데 이것도 아주 문제가 없진 않습니다.
사용자가 경우에 따라 여러 브라우저를 동시에 사용하고 있을때죠. 만약에 사용자가 크롬과 사파리를 동시에 띄우고 두 브라우저를 구분하지 않고 주문/결제 페이지로 이동해서 결제를 시도하다보면, 결과적으로 크롬, 사파리에서 결제 승인 요청을 진행하 2번의 결제 중복이 일어날 수 있습니다. 물론 이 정도 리스크는 감수 할만합니다. 아무리 사용자가 실수를 하더라도 단 한번의 결제 중복이 일어나며, 이런 실수를 하는 경우도 드뭅니다. 어떻게보면, 사용자의 과실이 아예없다고는 할 수 없으니, 사용자가 고객 서비스 센터에 문의하면 해당 결제만 취소해주는 식으로 운영을 해도 아주 심각한 문제는 없을 것 같습니다.
하지만, 제가 진행하는 프로젝트는 실제로 고객이 당장 사용해야할 정도로 급한 상황은 아니기에 좀 더 욕심을 내보도록 합시다.
case4: 결제/주문 페이지로 이동한 순간 결제 주문번호 생성 후 해당 번호를 활용
위에서 잠깐 정리한 사용자의 상품 구매 프로세스를 참고하여, 사용자가 정말로 구매를 결심하는 지점에서 멱등성을 부여할 수 있는 번호를 생성하는 것이죠.
다만 이 단계는 주문이 확정된게 아니라, 사용자가 결제하기를 확정지은 것이기 때문에 주문 도메인의 주문 번호와는 별개로 지정해야합니다. 그래서 네이밍도 '주문번호'가 아니라, '결제 주문번호'라고 지었습니다. 이제 이 '결제 주문번호' 지정에 어떤 규칙을 적용하느냐가 문제일텐데요.
결국 장바구니를 거치든 상세페이지를 거치든 사용자는 보통 구매를 결심하면 결제 페이지로 이동하게 되어있습니다. 이러한 사용자 경험을 토대로 '결제 주문'을 관리하는 테이블을 활용하여, 사용자가 결제 주문 페이지에 방문하는 순간 해당 테이블에 상태를 추적하는 레코드를 추가하는 것입니다. 해당 레코드에는 주문 결제를 식별할 수 있는 pk와 사용자의 id, 그리고 비즈니스 룰에 맞는 무작위의 문자열이 포함되어 있습니다.
그 레코드의 pk를 활용하여, 주문 결제 페이지로 이동할 수 있는 url로 리다이렉트 시키고 그 pk를 통해 검색한 레코드와 사용자 인증 정보 등을 확인하여 결제의 무결성을 확인하고, 주문 결제 레코드의 내용을 조합하여 멱등키를 생성하여, 사용자의 실수로 인해 발생하는 중복결제를 방지하는 것입니다.
결론
여러가지 방향을 생각해보았는데, 현재로서는 ‘case4’가 가장 타당한 방법일 듯 합니다. 프론트에서 사용하는 정보를 활용하는 방법은 악의적인 사용자에게 정보를 조작할 구실을 줄 뿐이기에 될 수 있으면 결제의 무결성 확인과 멱등성을 부여하는 과정은 서버에서 처리하는 것이 안전하고, 또 개발 난이도 역시 그렇게 높지 않습니다.
'기술적 이슈 정리' 카테고리의 다른 글
카프카를 선택한 이유: 높은 내구성과 고가용성, 단일 실패지점을 극복하는 로그 시스템 갖추기 (1) | 2023.12.09 |
---|---|
payment-lab 기술적 이슈 -3- 결제 이력 및 복구, Logger를 그대로 사용해도 되는걸까? (0) | 2023.11.17 |
payment-lab 기술적 이슈 -1- 로깅 중 비밀번호 노출의 위험성 및 대비책 (1) | 2023.11.12 |
정확한 결제 상태 추적 이슈 -1-, StateMachine 에 대해 알아보자 (1) | 2023.08.23 |
이벤트는 무엇이고 언제 사용해야할까 (0) | 2023.08.22 |