청구/수납 서비스 개발기
이 글은 사내 블로그에 작성한 청구/수납 서비스 개발기 내용을 그대로 가져오면서 나의 블로그의 언어톤에 맞게 변경한 글이다.
엊그제 2023년을 축하했던 것 같은데 벌써 2월이 지나가고 있다. 다들 느끼겠지만, 시간이 참 빠른 것 같다.
작년 키친보드는 많은 것을 이루었고 바꾸었다. 정비기간을 통해 서버 언어를 전환하였고, 기존에 제공하던 정리 서비스가 아닌 주문 서비스를 새롭게 런칭하였다. 그리고 도도 카트에서 키친보드로 서비스명을 변경하면서 우리 서비스가 전달하는 가치를 사용자가 좀 더 명확하게 인지할 수 있도록 하였다.
주문 서비스에 대한 시장성을 확인한 우리는 2022년 목표를 설정하여 이를 달성하기 위해 열심히 달렸고 마침내 목표했던 KPI를 달성하는 쾌거를 이루었다.
키친보드는 주문 서비스의 성공적인 출시에 힘입어 고객에게 가치를 전달하기 위한 다음 스텝으로 청구/수납 서비스를 런칭하였다. 이 글에서는 청구/수납 서비스를 개발하면서 겪었던 여러 이야기들을 소개하고자 한다.
청구/수납 서비스?
여러분이 공과금을 납부하는 방식과 유사한 형태로 생각하면 이해하기 쉬울 것 같다. 매장은 식자재를 납품해주는 유통사에 음식을 조리하기 위한 식자재를 주문하게 된다. 주문마다 거래대금을 납부하는 형태가 아니라 외상과 같이 일정 기간의 거래대금을 한꺼번에, 유통사에 결제하는 형태를 가지는데, 이때 유통사는 매장에 거래대금 납부를 요청하는 청구서를 발행하게 되고 매장은 이 청구서에 맞는 거래대금을 결제하는 방식을 청구와 수납이라고 한다.
키친보드는 요식업을 하는 매장과 해당 매장에 식자재를 납품해주는 유통사를 좀 더 세련되게 연결해주는 제품이다. 주문/채팅 서비스가 매장이 유통사에 식자재를 주문하는 세련된 방법을 제공하였다면, 청구/수납 서비스는 유통사가 매장에 거래대금을 청구하고 매장이 청구된 금액을 수납하는 방식을 좀 더 세련되게 제공하고자 하는 것이 목표이다.
식자재 시장에서의 청구/수납
일반적인 청구/수납
아마 여러분이 청구와 수납을 떠올린다면 대표적으로 가스비, 수도 요금, 전기요금과 같은 공과금을 납부하는 방식을 생각하게 될 것이다. 그중에 전기요금으로 예를 들어 보겠다.
한국전력공사에서는 전월 여러분이 사용한 전기 사용량을 기반으로 전기요금 청구서를 발행하게 된다. 여전히 우편으로도 많이 활용하고 있지만 최근 카카오톡으로도 청구서를 발행하기도 한다. 청구서를 받은 여러분은 청구 금액을 확인한 후 가상계좌나 카드 결제, 간편결제 등 다양한 결제 수단을 통해 전기요금을 납부하게 되면서 해당하는 달의 전기요금의 청구와 수납의 생명주기는 종료되게 된다.
유통사와 매장에서의 청구/수납
식자재 유통사와 매장에서도 청구와 수납의 생명주기도 유사하다. 매장이 이전 기간 주문한 식자재 가격을 기반으로 유통사는 매장에 거래대금 청구서를 발행하게 되고 매장은 해당 청구서를 확인하고 다양한 결제 수단(대부분은 계좌이체)을 통해 청구 금액을 납부하게 된다.
다만, 식자재 유통사와 매장 간의 청구와 수납에서는 미납금이라는 것이 존재한다. 거래금액이 워낙 크다 보니 매장의 자금 상황에 따라서는 청구한 금액을 모두 납부할 수 없는 상황이 존재하게 되고 유통사는 매장과의 지속적인 거래 혹은 여러 가지 이유로 인해 해당 기간의 청구한 청구 금액에 비해 부족한 금액으로 수납이 되는 경우에 미납금을 다음 청구로 이월시켜 다음 거래 기간의 거래금액과 미수금을 합쳐서 청구서를 발행하게 된다.
즉, 언제든 청구 금액과 다른 금액을 매장에서 납부할 수 있는 것이 이곳 시장에서는 일반적인 형태라 볼 수 있다. (전기요금도 미납이 존재하지만 납부하지 않는 경우에만 해당하고 분할납부도 특정 기간 혹은 사유에 의해서 신청이 가능하다.)
목표
주문 서비스의 목표는 기존의 카카오톡(혹은 문자)으로 식자재를 주문하던 매장과 유통사의 경험을 최대한 해치지 않으면서 식자재 주문 방식을 개선하는 것이었다. 그래서 주문 서비스에 채팅 기능을 함께 탑재하는 것이 중요한 요소 중 하나다. 이번 청구/수납 서비스의 목표 또한 유통사와 매장이 카카오톡(혹은 문자)을 통해서 이루어지던 청구/수납 경험을 최대한 해치지 않으면서 유통사에서는 좀 더 손쉽고 불편하지 않게 거래대금을 청구하고 매장에서는 편리하게 수납하고 수납내역을 확인할 수 있는 것들을 개선하고자 하였다.
현실과 이상
나의 짧은 경험으로 제품을 만들 때 고객이 원했던 제품이 만들어지지 않았던 이유는 제품팀에서 생각했던 이상적인 모습과 실제 고객이 현실에서 느끼는 필요성에 대한 차이점 때문이라 생각한다. 앞서 일반적인 청구/수납과 유통사와 매장의 청구/수납을 소개했던 이유도 크지 않을 수 있지만 제품의 기능을 만들 때 우리가 일반적으로 생각했던 청구/수납과 실제 유통사와 매장에서 이루어지고 있는 청구/수납에 차이가 있었기에 소개를 한 것이다. 이러한 부분들로 인해 제품팀에서는 제품을 개발하기 전에 요구사항을 정의하는 것에서부터 많은 시간을 투자하였다. 그럼, 대표적으로 어떤 것들이 있는지 한번 보자.
청구 금액에 맞게 수납이 이루어지지 않는다.
만약 여러분이 전기요금 청구서에 적힌 금액에 맞지 않는 금액을 수납 계좌로 이체한 경우는 어떻게 될까? 가상계좌로 이체하는 경우 과오납 체크를 하므로 이체가 되지 않는다. 즉, 청구된 금액만큼 수납하여야 청구에 대한 수납이 완료되는 것이다.
하지만 앞서 유통사와 매장 간의 청구/수납에서 말했다시피 매장은 상황에 따라 유통사가 청구한 금액보다 적은 금액을 납부하기도 한다. 그래서 가상계좌 발급을 통한 청구와 수납의 생명주기를 온전히 따르기에는 무리가 있었고 과오납 체크 또한 수행할 수 없는 이슈가 있었다.
고객 전용 계좌를 사용해야 한다.
우리의 목표는 매장과 유통사의 기존 경험을 최대한 해치지 않으면서 식자재 주문 방식을 개선하는 것이다. 앞서 청구 금액에 맞는 수납이 어렵다는 이유 말고도 기존에 청구와 수납의 경험을 해치지 않는 요소 중 하나가 바로 고객 전용 계좌를 두고 계좌이체를 하는 것이다. 기존에는 아래와 같이 유통사가 보유하고 있는 계좌를 지정해두고 매장에 아래와 같이 입금요청을 보내는 방식으로 청구를 진행하였다.
매장에서는 유통사에 맞춰 계좌를 개설해놓고 이체를 할 수 있기에 이체 수수료에 대한 부담이 없고, 매번 계좌번호가 바뀌지 않기에 기억해두거나 최근 거래내역을 통해 이체를 할 수 있기에 편리함이 있다.
하지만 청구할 때마다 새로운 가상계좌를 발급하여 수납을 요청한다면 청구한 금액과 일치하는 수납을 기대할 순 있지만 청구 금액보다 적은 금액의 수납을 허용하는 지금까지의 매장과 유통사의 청구/수납 프로세스를 갑작스럽게 변경하는 문제가 있기도 하고 가상계좌번호를 매번 새롭게 기억해서 이체해야 하는 번거로움 또한 발생하게 된다.
미납금 추적이 힘들다.
키친보드는 유통사에서 사용하는 ERP를 대체하는 제품이 아니다. 그러다 보니 유통사가 ERP에서 이미 사용 중인 데이터를 키친보드의 데이터로 가져오는 것이 관건인데, 아쉽게도 유통사에서 직접 키친보드에 미납금을 동기화해주지 않는다면 ERP의 미납금을 추적할 방법이 현재로선 딱히 존재하지 않는다.
이상적으로는 미납금 관리가 되어서 청구서에 미납금과 직전 거래금액이 합쳐진 금액이 청구 금액으로 표시되고 수납 시 미납 잔액과 연동되어 표시되면 좋겠다는 생각이 든다. 유통사에서 일일이 미납금 데이터 동기화를 해주면 좋겠지만 고객에게 번거로움을 줄 수 있고 사람이 직접 해야 하는 일이다 보니 누락이 생길 수 있다. 그래서 미납금을 어설프게 표시하여 고객에게 혼란을 주기보다는 미납금 관리를 과감하게 포기하고 청구 시 고객이 원하는 청구 금액을 입력할 수 있도록 자유도를 높이는 방법으로 우회하였다.
청구 없이 수납할 수 있다.
앞선 문제를 해결하기 위해 고객 전용 계좌를 사용하게 되면 어떤 이슈가 있을까? 바로 청구 없이 수납이 가능하다는 것이다. 실수든 고의든 고객 전용 계좌가 생성이 되고 매장에 계좌번호가 공개된다면 매장에서는 언제든지 수납금을 이체할 수 있다. 키친보드에서는 미납금을 관리하지 않기에 유통사가 청구서를 발행하지 않더라도, 혹은 청구된 금액보다 미납금이 크더라도 매장에서는 평소에 해오던 대로 계좌이체를 통해 수납을 할 수 있는 것이다.
그렇다면 우리가 생각하는 일반적인 청구와 수납 프로세스를 지키기 위해 이러한 수납을 하지 못하도록 막아야 할까? 제품을 위해서 고객의 기존 경험을 해치는 것은 저희가 목표로 하는 제품의 방향과는 다르다고 생각한다. 그렇다면 유통사와 매장의 프로세스를 지키면서 키친보드에서 제공하는 청구/수납 기능이 어색하지 않도록 잘 풀어나가는 것이 관건이었다.
해결 과정
기존의 유통사와 매장의 프로세스를 최대한 해치지 않으면서 좀 더 편리하고 유용한 청구/수납 경험을 제공하기 위해 우리 제품팀은 모두가 모여 며칠 동안 청구/수납에 필요한 기능들을 정의하고 필요하거나 혹은 불필요한 것들을 조정해나갔다. 이때 이상적인 청구/수납 방식에 매몰된 나머지 실제로 유통사와 매장에서는 불편해할 기능들도 제안되기도 하였고 너무 유통사와 매장의 입장만 생각한 나머지 제품의 정체성을 흐리는 제안도 나오기도 하였다. 사업팀에서도 현재 시장 상황을 잘 이야기해주면서 제품팀이 최선의 선택을 할 수 있도록 많은 의견을 주기도 하였다.
그러한 노력 끝에 지금 시점에 도출해낼 수 있는 최선이 선택하게 되었고 그 결과물이 지금 출시된 청구/수납 서비스이다. 사실 이렇게 논의를 마치고 제품 개발을 진행하였음에도 불구하고 개발 중간중간에도 QA를 진행하고 있는 와중에서도 이상적인 부분에 대한 의견이 다시금 나오기도 하였다^^;; 하지만 우리 팀은 충분히 고민하고 조율한 끝에 내린 결론이었기에 새로운 의견이 나와도 목표 지점을 잃지 않고 원하는 시점에 계획했던 제품을 성공적으로 출시할 수 있게 되었다.
나는 언제든 제품을 만들면서 현실과 이상 간의 괴리는 언제든 발생할 수 있다고 생각한다. 다만, 이 괴리를 모두가 조정하고 합의한 다음 제품을 만들어 나가야 팀의 목표를 놓치지 않고 정체성을 가질 수 있다고 생각한다.
유저스토리
앞선 논의를 통해 도출해낸 요구사항들을 토대로 아래와 같이 유저스토리들이 도출되었다. (관리기능과 주요하지 않은 스토리들은 제외하였으니 이 부분은 참고바란다.)
- 유통사는 ERP 데이터를 이용하여 청구서를 일괄 생성할 수 있다.
- 유통사는 생성한 청구서를 취소할 수 있다.
- 유통사는 활성화된 청구서를 조회할 수 있다.
- 유통사는 미납금이 존재하는 매장들에 미납알림 메시지를 발송할 수 있다.
- 유통사는 매장이 계좌이체를 통해 청구 금액을 수납하면 메시지를 수신할 수 있다.
- 유통사는 자신과 거래 중인 매장들의 수납명세를 조회할 수 있다.
- 유통사는 정산 예정 내역을 조회할 수 있다.
- 매장은 유통사가 청구서를 발행한 경우 메시지를 수신하고 청구서를 확인할 수 있다.
- 매장은 청구 금액을 납부한 경우 납부 완료 메시지를 수신할 수 있다.
- 매장은 유통사가 발송한 미납알림 메시지를 수신할 수 있다.
- 매장은 자신의 수납내역을 조회할 수 있다.
흐름도
사실 유저스토리만 보면 “현실과 이상”에서 말했던 고민거리들이 표현되지 않아 문제가 없지 않은가에 대한 의문을 가질 수 있으리라 생각한다. 그래서 흐름도를 통해서 좀 더 유통사와 매장의 청구와 수납 흐름을 좀 더 자세히 표현해보겠다.
일반적인 청구와 수납의 경우
먼저 유통사와 매장에서의 일반적인 청구/수납 흐름도를 보자. 전기요금을 수납하는 방식과 비교했을 때 큰 차이가 없다.
- 유통사는 지난 거래대금을 입력하여 청구서를 매장에 발행한다.
- 매장은 청구된 금액만큼 지정된 계좌에 이체를 통해 거래대금을 수납한다.
- 시간이 지난 후 유통사는 새로운 청구서를 발행하게 되고 기존에 발행된 청구서는 자동으로 만료된다.
- 매장은 청구된 금액만큼 지정된 계좌에 이체를 통해 거래대금을 수납한다.
청구 금액보다 적은 금액을 납부하는 경우
다음으로는 유통사와 매장에서 흔하게 발생하는 경우인 청구 금액보다 적은 금액을 납부하는 경우를 다루어보겠다.
- 유통사는 지난 거래대금을 입력하여 청구서를 매장에 발행한다.
- 매장은 청구된 금액보다 적은 금액을 한번 혹은 여러 번 나누어서 납부한다.
- 시간이 지난 후 유통사는 새로운 청구서를 발행한다. 다만 해당 청구 금액은 미납금 + 거래대금을 유통사가 직접 입력하여 발생한다.
- 매장은 청구된 금액보다 적은 금액을 한번 혹은 여러 번 나누어서 납부한다.
아마 여기서 여러분은 위 흐름도에서 아래의 2가지 의문점을 가질 수 있을 것이다. 바로 이 부분이 우리가 마주한 이상과 현실의 괴리 중 첫 번째이다. 앞서 말한 바와 같이 현재로선 완벽하게 청구의 종료 시점과 미납금을 관리하기 어렵다고 판단하였고 이에 대해 아래와 같이 타협점을 찾았다.
처음 발행된 청구서의 청구 금액이 다 수납되지 않았는데 청구서가 만료되었다는 것을 어떻게 알 수 있는가?
매장이 정확히 청구한 금액을 수납한다는 것을 보장하기 어려우므로 청구서의 생명주기를 다음 청구서가 생성될 때까지로 정하였다. 그래서 다음 청구서가 발행되는 시점에 직전 청구서를 만료하는 방법으로 우회하였다. 그럼, 청구서와 수납과의 관계가 모호해지지 않는가? 라는 질문을 할 수 있다. 물론 그럴 수 있다. 하지만 대부분의 경우 유통사에서는 거래대금을 매장에 요청하기 위해 청구서를 새롭게 발급할 것이고 자연스럽게 청구와 수납 간의 관계가 원하는 대로 맺어질 것이라 기대하고 있다.
다음 청구서의 청구 금액이 미납금 + 거래대금인데 미납금이 자동으로 계산되지 않고 직접 입력해야 하는가?
앞서 말했다시피 키친보드는 유통사에서 사용하는 ERP를 대체하지 않는다. 그래서 ERP에서 관리되는 미납금을 정확하게 추적하고 동기화하기가 어렵다는 이슈가 있다. 유통사에 미납금을 동기화해달라고 할 순 있지만 관리 포인트가 늘어나므로 고객의 경험을 해친다는 점과 유통사 담당자가 수기로 관리하기 때문에 누락 및 오입력과 같은 이슈가 있어 신뢰성 있는 미납금 관리가 현실적으로 어려울 수 있다고 판단했다. 돈을 다루는 부분에서는 모호한 데이터를 표시하기보다는 유통사가 원하는 청구 금액을 손쉽게 입력하여 매장에 전달할 수 있도록 하는 것에 집중하였다.
청구 없이 수납하는 경우
다음으로는 청구서가 발행되지 않았음에도 매장이 수납하는 경우를 다루어보겠다.
- 매장은 공유된 계좌번호로 유통사에 미납금을 납부한다.
- 유통사는 지난 거래대금을 입력하여 청구서를 매장에 발행한다.
- 매장은 청구된 금액만큼 지정된 계좌에 이체를 통해 거래대금을 수납한다.
- 시간이 지난 후 유통사는 새로운 청구서를 발행하게 되고 기존에 발행된 청구서는 자동으로 만료된다.
- 유통사는 잘못 발생한 청구서를 취소한다.
- 매장은 공유된 계좌번호로 유통사에 미납금을 납부한다.
일반적인 청구와 수납구조를 이해하고 있다면 이상해 보일 것이다. 왜냐하면 청구 없이 수납이 존재하기 때문이다. 앞서 여러 이유로 인해 가상계좌가 아닌 고객 전용 계좌를 사용하게 되었다고 말했었다. 매장에 계좌번호가 한번 공유되고 나면 매장에서는 언제든지 미납금에 대해 수납을 할 수 있다. 심지어 키친보드에서는 미납금을 정확히 알기 어려우니 해당 수납금이 과납인지, 혹은 오납인지조차 알 수도 없다.
그래서 우리는 과감하게 청구와 수납 간의 관계를 느슨하게 가져가 보기로 하였다. 청구서 없이도 매장은 언제든지 수납을 할 수 있고 만약 활성화된 청구서가 존재한다면 수납과 청구와의 관계를 선택적으로 맺어주는 형태로 개발하게 되었다.
설계문서
위 유저스토리와 흐름도를 기반으로 우리 백엔드 챕터에서는 아래와 같이 설계문서를 작성하였다. 평소에도 개발하기 전에 설계문서를 작성해왔지만, 이번 프로젝트에서는 개발자끼리도 오해가 생기지 않도록 더욱더 꼼꼼하고 자세하게 적으려고 노력하였다. 이번 프로젝트에서는 백엔드에서 작업할 내용이 많고, 고민해야 할 것들이 많다 보니 유달리 문서량이 많았던 것 같다 ^^;;
Message Throttling
개발기인데 개발적인 이야기가 빠진다면 섭섭할것 같다. 지금까지는 청구/수납 서비스에 대한 배경과 설계에 대한 이야기였다면 청구/수납기능을 개발하면서 겪었던 대표적인 이슈를 소개해보자 한다.
유통사와 매장은 기존에 카카오톡이나 문자를 통해서 주문도하고 소통도 하고 결제 대금 청구도 수행하였다. 그러다 보니 키친보드에서도 채팅 기능은 중요한 기능 중 하나이다. 우리는 주요 알림 및 채팅 기능을 샌드버드를 이용하여 제공하고 있다. 주문하거나 접수 완료할 때도 채팅 알림을 통해서 수행하고 이번에 청구 및 수납을 할 때도 채팅 알림을 통해서 매장과 유통사에 알림을 전송한다.
이슈는 아래 유저스토리에서 다량의 메시지를 동시에 발송해야 하는 요구사항에서 발생하였다.
유통사는 ERP 데이터를 이용하여 청구서를 일괄 생성할 수 있다.
유통사와 거래하는 여러 매장들에 일괄적으로 청구서를 발행한다면 키친보드 서버에서는 샌드버드의 Platform API를 통해 유통사와 매장이 들어가 있는 채팅방에 일괄적으로 청구서가 생성되었다는 메시지를 전달하게 된다.
문제는 샌드버드의 Platform API에 제약조건이 있다는 것이다. 채팅방에 메시지를 전송할 때 /{channel_type}/{channel_url}/messages
API를 사용하는데 초당 5건의 요청 제한이 걸려있다. (실제로 테스트해보면 일시적으로 요청하는 정도라면 초당 10개 정도까지는 받아주는 듯 하다.)
결국 유통사에서 다수 매장에 청구서를 발행하게 되면 샌드버드의 메시지 전송 제약에 의해 채팅 메시지를 실패할 수 있다는 우려가 제기되었다. 실제로 테스트 해보았을 때도 30개의 매장에, 동시에 청구서를 발행하는 경우 오류가 발생하는 것을 확인할 수 있었다.
{
"error":true,
"message":"Too many requests (scope: user, limit: 5;w=1, remaining: 0, retry_after: 0.16522979736328s, reset: 1.9652297496796s).",
"code":500910
}
그럼, 위 문제점을 어떻게 해결할 수 있을까?
Sendbird의 Announcement Message 활용
Sendbird는 Announcement Message를 통해 다수의 사용자(최대 2만 명)에게 메시지를 전송할 수 있도록 기능을 제공해준다. 목적 자체가 다수의 사용자에게 메시지를 전달하는 것이기에 Platform API가 가진 사용자별로 초당 5개의 메시지 전송 제한을 두고 있지 않아 고려해봄직하였다. 키친보드에서도 유통사의 공지사항을 전파하기 위한 용도로 이미 사용하고 있기도 해서 연동도 큰 문제는 되지 않았다.
하지만 Announcement Message는 다수의 사용자가 동일한 메시지만 수신할 수 있다. 앞서 보여준 청구 메시지를 보시면 알겠지만, 매장이 수신하는 청구 메시지는 청구 금액과 계좌번호, 예금주가 매장마다 다르게 표시되어야 한다. 그래서 아쉽게도 Announcement Message는 사용할 수 없다는 결론을 내렸다.
Message Queue의 Delayed Message 활용
다음으로 제안되었던 방법은 Message Queue에서 제공해주는 Delayed Message를 활용하는 것이다. ActiveMQ나 RabbitMQ에서는 Delayed Message 기능을 제공한다. Delayed Message는 쉽게 말해서 MQ에서 메시지를 원하는 시간만큼 지연 발송해주는 기능이다.
Platform API가 초당 5개의 메시지 전송 제한이 있으므로, 0.2초당 하나의 메시지만 발송하도록 지연 발송한다면 문제없이 해결할 수 있으리라 생각했다. 물론 Thread.sleep(200)
과 같이 서버 내 코드로 지연발송을 사용할 수 있다. 하지만 서버 내 코드에서 지연발송을 구현한다면 아래와 같은 문제점이 발생한다.
비즈니스 코드가 외부 의존성을 의존한다.
일단 코드 수준에서 좋지 못한 디자인을 유발한다. 도메인 코드는 외부 서비스에 대한 의존성을 최소화하는 것이 좋다. 그래서 외부 의존성인 Sendbird의 제약조건을 비즈니스 코드에 녹이는 것은 바람직하지 않다고 생각했다. 극단적으로 혹시나 채팅 서비스를 Sendbird가 아닌 다른 서비스로 교체한다고 가정해보겠다. 다른 서비스는 Sendbird와 다른 제약조건을 가질 수 있다. 그래서 외부 의존성의 변경이 도메인 코드의 변경을 유발하게 되는 좋지 않은 상황이 발생하게 된다.
API의 성능저하를 유발한다.
0.2초의 지연발송을 publisher 쪽에 구현한다면 메시지를 발송하는 Application Server의 API 성능은 최소 0.2초 이상의 응답시간을 가지게 된다. 이는 비동기 발송으로 풀어낼 수 있지만 애플리케이션 내 비동기 발송은 메시지 유실과 같은 또 다른 문제를 야기한다.
메시지 유실 가능성이 존재한다.
앞서 API의 성능을 올리기 위해 채팅 메시지 발송을 비동기로 처리할 수 있다. 서버는 별도의 스레드에서 메시지 발송을 대기하게 된다. 다만 예상치 못한 상황에서 서버가 다운되는 경우 사용자는 청구서 발송이 성공했다는 응답을 받았지만, 채팅 메시지를 받지 못한 상황이 발생할 수 있다. 서버가 Graceful shutdown을 제공한다면 그나마 이러한 상황을 방지할 최소한의 안전장치를 마련했다고 할 순 있겠다. 하지만 프로그래밍에 있어서 0.2초는 참으로 긴 시간일 수 있기에 여전히 위험성을 가지고 있다고 생각했다.
위와 같은 문제로 우리는 좀 더 안정적으로 지연 메시지를 사용하기 위한 방법으로 Message Queue를 사용하는 것이 좋다고 판단하였다. 하지만 Delayed Queue를 사용해도 앞서 말한 문제를 근본적으로 해결할 수 없었다. 그 이유는 바로 서버의 가용성과 성능향상을 위해 Scale-out 되어 있었기 때문이다.
좀 더 이해하기 쉽게 그림으로 표현해보겠다. 만약 여러 유통사가 청구서를 발송한다고 가정해보겠다.
위 그림에서 볼 수 있다시피 Message Queue Delayed Message는 서버에서 요청 시 얼마나 메시지를 지연할 것인지를 결정하기 때문에 Scale-out 된 서버에서 각각의 서버가 동시에 지연 메시지 발송요청을 하는 경우 Sendbird의 API 요청 제한을 초과하게 될 가능성이 존재했다.
Consumer Message Throttling 활용
위 문제점들을 해결하기 위해 최종적으로 도출된 방법은 바로 메시지를 소비하는 Consumer에서 Message Throttling을 수행하는 것이다. 즉 Messge Queue로 전달하는 메시지는 지연 발송하지 않고 즉시 전송하게 되고 메시지를 소비하는 쪽에서 Throttling을 걸어서 Sendbird의 API 제한조건에 맞도록 전송하도록 하는 것이다.
하지만 이 방법도, 만약 Consumer가 Scale-out 된다면 앞서 말한 Sendbird 의 API 요청 제한을 초과할 가능성은 여전히 남아있게 된다.
우리는 이 부분을 해결하기 위해 라이브러리의 힘을 빌리기로 하였다. 바로 bucket4j이다. buket4j는 Token bucket이라는 알고리즘을 기반으로 분산 환경에서도 원하는 속도제한을 수행할 수 있도록 기능을 제공해주는 라이브러리이다. 즉, 여러 대의 Consumer가 존재하더라도 마치 하나의 Consumer가 지연발송 할 수 있도록 메시지 전송시간을 조절해준다.
한편 메시지 발송하는 쪽에서 bucket4j를 이용하여 지연발송 할 수 있지 않는가에 대한 질문을 할 수 있다. 다만 발송하는 쪽 즉, Publisher에서 Message Throttling을 하는 경우 앞서 말한 동기 코드의 경우에 성능 문제, 비동기 코드의 경우 메시지 유실과 같은 문제를 동일하게 내포하고 있기에 Message Queue를 통해 전달보증을 꾀하였다. 만약 Consumer에서 메시지 소비를 대기하다가 모종의 이유로 서버가 다운되어 API 전송에 실패하는 경우 Message Queue는 재시도를 요청할 것이고 이때 메시지를 소비할 수 있는 서버가 해당 메시지를 소비하여 사용자에게 메시지를 반드시 전달해주게 될 것이다.
Message Throttling에 대한 간단한 튜토리얼을 만들어두었으니 해당 코드를 참고하자.
마무리
지금까지 청구/수납 서비스를 개발하면서 고민하였던 부분과 개발적인 이슈들을 살펴보았다. 이번 글은 개발적인 내용보다는 일반적이지 않은 유통사 시장의 청구와 수납에 대한 이해와 제품의 설계이유를 설명하기 위한 내용이 대부분이어서 조금은 아쉬울 수 있다는 생각이 든다. 다만 스포카에서 제품을 만들 때 고객을 위해 어떤 고민을 하고 더 나은 제품과 고객 경험을 제공하기 위해 어떠한 노력을 하고 있는지 여러분에게 소개할 수 있게 되었다는 점에서 나름대로 의미를 부여하고 싶다.
앞으로도 제품 개발에 대한 이야기를 많이 풀어보도록 하겠다.