As-Is
일반적인 그리고 기존 결제 흐름
- 클라이언트가 결제를 요청
- 서버가 결제를 진행하는데 필요한 정보를 응답
- 해당 내용을 토대로 pg사 호출
- pg사의 사용자 인증 이후 콜백
- 콜백받은 정보를 상태로 저장하고 있던 정보와 대조하고, 문제없으면 pg사로 승인 api 호출
- pg사의 승인 응답
- 승인 여부를 db에 저장 (결제 승인 응답, 로그, 대사작업에 필요한 정보 등)
- 회계처리를 진행하는 서버에 저장
To-Be
새로운 요구사항들 (배경)
- 회사 그룹의 오프라인 매장에서 판매된 상품까지 수선 서비스의 대상이 됨에 따라서 다른 계열사와의 협업이 필요하게 되었다.
- 온오프라인 자사 상품을 관리하는 별도의 서버와 통신하며 구현해야 했다.
- 의사결정된 내용은 비즈니스 로직과 기존 로직들은 기존처럼 처리하고 처리한 결과를 forward해서 on/offline 서비스에 저장, 그 외에 회계처리만 저쪽 서버에서 진행하기로 했다.
비즈니스 로직을 처리하고 포워드, 해당 서버로부터 전달 받은 데이터를 통해 비즈니스 로직을 처리하는건 별로 어려운 일은 아니었다. 기본적으로 동시성 이슈가 발생 할 일이 거의 없는 사용자 관련 데이터를 위주로 여러 데이터 소스에서 불러온 데이터를 기반으로 간단하게 검증하는 로직이 전부였다. 문제는 결제쪽이었는데..😕
새로운 요구사항들 (결제 관련)
- 기존과 동일한 결제 로직을 따라야 한다. 즉 주문 관련 데이터가 외부에 있지만, 회계처리 반영하는 것을 제외하고는 기존처럼 처리되어야 한다.
- 외부 서버 또한 만만치 않은 레거시 서버이기도 하고, 여러가지 사정상 큐 또는 메세징 시스템을 이용해서 요청을 직렬화 할 수 없다.
- 그 와중에 여러 결제 수단을 지원하고 싶으면서도 작업 공수를 줄이기(?) 위해 pg사의 신모듈을 붙여야 했다.
결과적으로 아래와 같은 흐름으로 작업을 정리했다.
간단한 컴포넌트 추가인데 복잡도가 상당히 증가했고 구현해야하는 인터페이스, 신경써야 할 것들이 너무 많았다.
BeforeStart
시작하기 전에 점검해야 한다고 생각했던 것들은 아래와 같이 세개였다.
기존 결제 로직을 외부 주문데이터와 연동 할 때 변경이 필요한 것들 점검
- 검토해봤을때, 로그성 테이블, 배치가 도는 대사작업 테이블들이 외래키 제약조건이 걸려있는것 빼고는 문제는 없었다.
- 운영팀에 해당 부분을 전달하고, 수선건의 주문 관련 테이블을 분리하고 별도로 처리할 수 있게 되었다.
- 내가 진행한건 간단하게 수선관련데이터에서 pk로 사용해도 문제없을지만 확인했다.
PG사 신모듈을 연동하기 위해서 바뀌어야 하는 인터페이스, 클라이언트 작업 점검
- pg사 인터페이스 자체는 변경이 거의 없었다. (정확히는 결제 sdk 호출을 위해서 필요한 데이터가 동일)
- 그러나 적재테이블 관련해서 다른 구현체를 사용하는 것 + 주문 모델과 pg사 승인 api호출 관련해서 엮여있는 부분이 많아서 그부분을 분리하는게 필요했다.
- 클라이언트쪽은 pg호출 데이터 자체는 동일하지만, 특정 domElement에 sdk가 결제 ui를 붙여주는 방식으로 변경되어 창 구조에 변화가 필요했다.
- 그리고 기존의 주문서 페이지는 메인 백엔드 서버가 뷰로 반환하고 그쪽에 로직이 있어 새로 클라이언트 결제 모듈을 구현해야 했다.
- 수선페이지는 next.js쪽에 있어 해당 결제 뷰 페이지로 리다이렉트가 어려웠기 때문
- 이부분때문에 여러 window간의 context와 데이터 관련해서 어려움을 겪을뻔 했는데, 당시에 진행하고 있던 본인인증 관련 작업들에서 사용한 postMessage로 해결했다. 관련한 내용은 신규-프로젝트에서-본인인증-연동-구현하기 참고
주문 관련 On/Offline Service 인터페이스 점검
-
해당 서버의 인터페이스를 확인했을때 문제점은 아래와 같았다.
-
기본적으로 그쪽에서는 수선정보를 조회해서 진행상태에서 결제가 가능한 상태이면 결제를 처리하고 처리결과를 전달하는 것을 생각하셨던 것 같다.
-
이러한 경우 결제가 중복으로 요청되거나, 인증 승인 이후에 외부적인 요인으로 결제가 불가능한 상태로 다시 바뀔 수 있다고 생각되었다.
-
이부분은 상태값을 추가하고, 해당 상태값에서의 변경은 우리쪽에서 가능하도록 수정을 요청드렸다.
-
이 부분에 대한 요청은 받아들여져서 아래와 같이 로직을 수정했다.
-
최종적으로 2번에서 결제건에 대한 조회를 하고,
-
해당 건을 결제 대기 상태로 만드는 api를 호출하도록 했다.
Impl
어떻게 우리쪽 상태와 외부 상태를 하나의 트랜잭션으로 엮을까?
…에 대한 정답은 이론상으로는 사실 알고 있었다. 주로 2pc와 outbox패턴을 이용한다.🥲
- 문제는 우리 서버가 오케스트레이션을 하려면 커넥션을 유지하는 동안에 우리쪽 로직을 다 처리하고 상태를 결제 완료 상태로 바꾸고 나와야 한다는 것인데, 이부분은 여건상 어려웠다.
- 다만 인증, 승인 단계 중간마다 로그를 테이블에 쌓는다 (주로 인증 콜백 요청값과, 승인 요청의 응답값), 이걸 통해서 복구로직을 조금 더 엄밀하게 작성하는 방향으로 개발했다.
- 이렇게 하면 정말 엣지케이스에는 수기처리가 필요하겠지만 수기처리를 포함해서는 최종적 일관성을 보장할 수 있다고 생각했다.
- 결제 단계별로 적재하는 로그가 outbox table과 아주 약간 비슷한 역할을 해준다고 생각했다. (다른 트랜잭션과 엮여서 커밋됨으로써 진행된 트랜잭션을 식별하는 정도만)
- 그리고 그 상태를 바탕으로 아래와 같이 로직을 작성했다.
Happy Case
- 결제요청
- 조회 api 호출 후 결제 정보 대조
- (화살표 방향이 반대지만🤪) 결제 상태 변경 api 호출 (to 결제 진행중)
- 결제 정보 응답
- PG사 호출 후 고객 인증
- 인증 콜백 (콜백 응답도 우리쪽 db에 저장된다)
- 결제 승인요청
- 승인 콜백
- 승인 데이터 저장
- 결제 상태 변경 api 호출 (결제 완료)
Unhappy Cases
- 이경우는 간단하다, 결제 상태가 진행중이면 이후의 요청은 버릴 수 있게 된다.
- 이경우는 on/offline service의 상태와 승인 응답로그 두가지를 보고 판단한다.
- 사실 인증 콜백응답의 로그도 저장하기에, 결제 진행중이고 인증 콜백 로그가 잘 남아있다면, 바로 승인을 진행해도 못할 것은 없다고 생각하긴했는데 일단 고객경험 등 의 이유로 콜백 요청 내에서만 승인 api를 호출하도록 하는것이 좋다고 생각했다.
- 뭔가 이상하게 결제가 안돼서 다시 했더니 다시 인증하는 과정 없이 계좌에서 돈이 나갔다?
- 사실 이부분은 뒤에 알게된 사실로 인해 생각은 바뀌었다.
이 부분에서 콜백로그만 남아있어도 승인 api 호출이 가능한 이유는 orderId는 승인 완료되기 전까지는 재사용이 가능하기 때문이다. 승인 완료 전까지는 paymentKey라는 식별값을 사용하며 orderId가 paymentKey가 맺어지는건 승인 이후이다.
추가적으로 저쪽 서버에서 회계처리 연동 이슈로 승인된 결제건의 결제 완료처리를 반려하는 경우가 많다, 이러한 경우 결제완료 api호출의 실패 응답을 받으면 무조건적으로 승인취소 api를 호출한다. 그부분은 오프라인 거래내역등 직접 확인해야 할 부분이 있어서 직접 확인후 수기처리를 원하셔서 그렇게 했는데 아마 아직까지도 그러한 방식으로 유지되는 것 같다.
Retro
todo : 나중에 알게된 사실 (socket timeout), 로컬 캐시 혹은 세션으로 상태를 저장했다면 (따닥을 막을 수 있음)