우리 서비스에는, AI 기능을 사용할 수 있는 가상재화 '크레딧'을 제공해주고 있었다.
하지만
등의 문제에 따라, 결제후 크레딧을 지급하게 하는 요구사항이 생겼다.
그래서, 해당 이슈를 받으며, 처리하며, 고도화하며 겪은 기술적인 내용과 생각에 대해 정리해보려고 한다.
아직..? 구현이 완벽한지는 모르겠다. 일단, 기본적인 틀을 갖추고 개선해나가려고 한다.
이슈에 대한 내용이 나왔을때, 그렇게 어렵지 않을거라고 생각했다.
PG사 결제 및 결제 핸들링은 기존대로 사내 다른 결제 MSA 와 워크스페이스 MSA 가 처리해주기로 했다.
우리팀은 주문번호 및 크레딧양을 전달하면, 주문번호에 대해 크레딧을 관리해주면 됐다.
다른팀과 API 명세 및 흐름에 대해 얘기를 하고 작업을 시작했지만? 구현을 하며 의사결정을 해야하는 부분들이 존재했다.
다른 팀과 통신은 HTTP 통신으로 결정됐다.
결제, 환불 행위 와 크레딧 충전, 차감 행위 를 메시지 기반으로 주고 받았으면 더 좋았을건 같지만..?
(그렇게 생각한 이유는 아래에 나온다.)
우리 서비스는 비용 구조상 독자적인 인프라를 운영하고 있다. (GPU 서버 비용 절감)
-> 다른팀과 메시지를 주고받을 환경이 갖추어져 있지 않았다.
그리고, MSA 에서 다른 서버에 부하를 주지않기 위해 최대한 짧은 요청-응답을 구성했다.
요청 -> 메시지 발행 -> 메시지 수신 & 처리 -> 결제 처리 완료 콜백 발행
이 구조에서, 데이터 불일치 가능성이 발생하게 됐다.
EX) 우리 서버는 데이터를 반영, 상대측이 결제 콜백 수신 못하면 데이터 불일치
이 과정에서 '콜백 수신 여부 상관없이 데이터 처리' vs '콜백 수신 못하면 반영 X'
두 가지중 선택을 해야했다.
=> 콜백 수신 여부에 상관없이 데이터를 처리하는 방식으로 결정했다.
결정의 근거는 크레딧이라는 데이터 오너십이 우리측이라고 생각했기 때문이다.
크레딧은 온전히 우리 서비스가 관리하는 데이터인데, 콜백을 못 받았다고 처리안해주는건 이상하다.
그럼에도, 요청이 실패한 요소는 별도 재시도가 가능하게 했다.
데이터는 3개의 테이블로 구성했다.
처음에는,현재 크레딧을 관리하는 테이블이 필요없다고 생각했다.
해당 테이블을 만들면 정합이 깨질 가능성이 생기기 때문이다.
하지만, 상법 64조 상
상행위로 인한 채권은 본법에 다른 규정이 없는 때에는 5년간 행사하지 아니하면 소멸시효가 완성한다.
물론, 크레딧을 서비스 정책상 5년보다 일찍 만료 시킬수 있지만
구매일 후 5년까지 크레딧을 환불을 해줘야 하는 의무가 존재한다.
그렇기에, 크레딧 만료기간을 비즈니스 정책상 5년으로 설정했다.(네이버 쿠키, 카카오 캐시등도 5년 유효)
-> 5년간 데이터를 전부 조회하는건 비효율적이라고 판단
물론, 시작일 ~ 어제의 데이터를 캐시에 서빙하는것도 가능은 하겠지만 오버 엔지니어링이라는 생각이 들었다.
=> 계속해서 UPDATE 로 크레딧을 수정하는 현재 크레딧 관리 테이블을 결정했다.
대신, 동시에 수정될 가능성을 방지하기 위해 두가지 처리를 했다.
크레딧 데이터가 수정되는 행위는
이였다. 이런 행위들이 동시에 실행되는 경우는 없다고 판단했다.
(충전과 차감이 동시에 들어올 순 없고, 사용자 요청상 동시에 사용이 두번 들어올 수 없다)
사용자 식별자 값을 기반으로, Redis LOCK 을 걸어 동시에 하나의 작업씩 처리해
크레딧이 동시 수정되는 가능성을 원천 차단했다.
혹시나, 데이터가 달라질 가능성은 언제나 존재하므로 새벽 시간 정합용 스케줄러도 추가했다.
크레딧 충전, 차감 내역을 조회해 총 크레딧 양
크레딧 사용, 복구 내역을 조회해 사용한 총 크레딧 양
을 합쳐 현재 크레딧 양과 일치한지를 검증했다.
두가지 INSERT ONLY 로그성 테이블로 변동하는 테이블의 정합을 보장했다.
이번에, 다른팀과 각잡고 의사소통 및 협업한게 사실 처음이였다.
우리팀은 다른 서비스 및 팀에 기능들을 제공해주는 형태라 팀내 의사소통 위주였다.
팀내 의사소통은 일종의 Agile 이였다.
궁금하면 슬랙 DM으로든, 대면으로든 바로 질문 및 얘기가 가능했다.
하지만, 다른팀과 얘기할 때는 PM 및 개발자분들과 회의를 잡거나
공개된 채널에서 정제된 대화를 이어나가야 했다.
이때, 한번의 회의가 무의미하게 끝나면 6~7 * 1.5 시간 시급 이라는 엄청 비싼 비용이 발생한다.
그래서, 이런 불필요한 비용을 줄이기 위해 노력했다.
LLM 을 통해 코드의 구현속도는 비약적으로 빨라졌다.
하지만, 단순 코드를 구현한다고 문제가 해결되지 않는걸 깨달았다.
이번에 다양한 정답지를 피해갔다.
두 서버가 로직상 데이터 교환을 한다.
-> 메시지를 통해, HTTP 재시도 및 타임아웃을 고려하지 않는다.
=> 당장, MQ 를 연결할 수 없어서 HTTP 요청 방식으로 구현한다.
응답에 메시지 전송을 보장한다.
-> Transactional Outbox Pattern + CDC 를 사용해서 비동기로 전송을 보장한다.
=> 팀내, 처음 도입하는 구조이므로 적용하지 않고 일단 동기 응답으로 보장한다.
사용자가 결제한 크레딧을 관리한다.
-> 캐시 레이어에 이전 데이터를 쌓아두고 오늘 데이터를 조회해 정합을 보장한다.
=> 정합을 허용하되 최대한 방지하고, 스케줄러로 정합을 맞춘다.
물론, 적은 내용이 정답이 아닐수도 있다. 방향이 틀릴수도
저대로 구현하면 더 어려울수도 있을것이다.
우리는 정답이 아니라, 해결을 해야한다.
해결을 하다보면 어떤 선택을 해도 마음에 들지 않을수 있다.
중에서 최악이 아닌, 차악을 고르기 위해 결정을 해야하는걸 느꼈다.
코드를 요구사항을 이해한 내용대로, 그대로 작성하면 완성은 될 것이다.
하지만, 퍼즐처럼 다른팀과 다른 서비스와 결합하다보면 엣지 케이스 및 모호한 부분들이 생긴다.
예를 들어,
크레딧 사용중 오류가 발생하면 크레딧을 복구해준다.
우리 서비스 로직의 흐름에선 당연한 로직이다. (안그러면, 사용자가 분노할 것이다..)
하지만, 상대 서비스에서는 크레딧 잔여량이 있으면 탈퇴 및 멤버 제거를 막는다.
환불을 해줘야하는 의무가 있기 때문이다.
각각의 서비스에선 당연한 정상 동작인데, 결합하면 문제가 된다.
이런 문제는 코드만 잘 짠다고 해결이 되는 영역이 아니다.
어떤 팀이 주체를 가져야하는지, 어디서 검증해야 구멍 및 영향이 적은지 등등
사람이 합의해야 하는 영역이다.
우리는, 더 나은 방향을 위해 끊임없이 고민하고 결정해야만 한다.
아직 실 배포 및 운영은 안되었지만, 감정을 남기기 위해 + 로직의 흐름상 문제가 없는지 다시 확인할 겸 회고를 일찍 진행했다.
역시, 결제 로직 관련 내용이 얽히면 생각할 것도 많고 법적으로 제약이 되는 것들도 많은걸 느꼈다.
그럼에도 이런게 개발자의 역할과 재미 아니겠는가 🫠