![[중간발표] B2B2C SaaS 대기열 서비스](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZcoNe%2FbtsJ1H9CgOw%2FE3EpIWVgIW3lv4TOecpOA0%2Fimg.png)
중간발표 자료








Monorepo를 통해 멀티모듈 구조를 채택했고, 루트 프로젝트에서 각각의 서브 프로젝트를 관리하고
versions.properties를 통해 여러 서버에서 사용하는 JWT 같은 의존성의 버전을 통합관리했다.
브랜치 전략으로는 main-dev-hotfix-feature로 이슈를 발행한 후 해당 브랜치를 파고, PR과 코드리뷰를 통해 이슈와 브랜치를 닫는 전략을 사용했다. 또한 sprint 단위로 일정을 관리했다.


이번 프로젝트에서 기획한 서비스는 B2B2C로, 서비스의 사용자는 대기열 서비스를 원하는 기업의 개발자가 될 것이며, 해당 기업은 엔드포인트 사용자에게 서비스를 제공하는 구조로 이루어져 있다.


처음에 각자 개발하고 싶은 부분을 고민하다가, 개발자를 위한 서비스를 만들면 어떨까라는 의견이 나왔다.
AWS처럼 종량제 과금방식으로 사용하기 쉬운 SaaS 서비스를 제공하는 것을 생각했고, 대신 높은 트래픽을 대신 처리해주는 대기열 서비스를 선택해 개발 난이도를 높였다. 그에 따라오는 장점으로 B2C 개발 대비 CRUD 작업이 줄어들어 핵심 로직 구현에 집중할 수 있었고, 대용량 트래픽에 대응되는 아키텍처를 구상해보면서 시야를 넓힐 수 있었다.

대기열이 필요한 서비스로 크게 2개를 구상했었는데, 처음은 은행창구였다. 방문하는 모든 사용자에 대해 대기열을 세워야 하고, 순서를 보장하고, 중복이 존재해도 된다는 특징을 가지고 있다.

다음은 티켓 발급 서비스이다. 한정된 수량이 존재하기에 그 외의 요청에 대해선 필터링이 필요하지만, 기본 대기열이 필요하고, 순서가 보장되어지되 중복이 일어나서는 안 된다. 이 2개의 서비스 간의 공통점을 기본 제공 스펙으로 차이점을 옵션 스펙으로 가져가려 했다.


이에 따라오는 첫번째 고민이 대기열을 어떻게 구현할 것인가? 였다.
대기열은 매우 짧은 시간 내 많은 입출력이 오가고, 순서가 보장되어야한다. 가장 많이 사용하는 것이 Redis이다.
메모리 기반으로 데이터를 저장하기에 빠른 I/O가 가능하며, 싱글 스레드 기반이기에 순서가 보장된다는 특징이 있다.
하지만 우리팀은 네카라쿠배 같은 대기업에서도 사용할 수 있다는 점을 고려했다. 이에 Redis가 그 많은 트래픽을 처리할 수 있는 처리량을 가지고 있는가? 또한 Redis가 다운되었을 때, 데이터가 유실되는 점을 생각해 다른 방안을 생각해봤다.

Kafka는 Redis만큼의 짧은 지연시간과 더불어 높은 처리량을 가지고, 확장성 또한 뛰어나다는 장점이 있다. 또한 Kafka Producer의 메시지 전달 보장 옵션을 변경하면서 다양한 옵션을 사용자에게 제공할 수 있다는 장점이 있어서 앞단 대기열을 Kafka로 받자는 선택을 했다.

처음에는 B2B2C의 C쪽에서 우리팀이 발급한 token을 통해 사용자 서버를 우회해서 트래픽을 받자는 생각이였지만, 보안적으로 취약했기에 '우리 서비스를 사용하려면 그 정도 트래픽은 감당해라'라는 기획적인 측면으로 해결했다. 그리고 사용자 서버에서는 우리팀이 제공한 라이브러리를 통해 쉽게 통신할 수 있도록 할 예정이다.

이제 생각해야 되는 점이 '대기열의 정보를 어떻게 처리할 것인가'이다.
그에 따른 2가지 방법 중 첫번째는 대기열에 변경이 있을 때마다 실제로 업데이트해서 정합성을 제공하는 것이고, 두번째는 대략적인 처리량으로 예상 순번과 예상 남은 시간을 추산하는 것이다. 우리팀은 먼저 첫번째 방식으로 구현하고, 추후 쌓인 데이터를 기반으로 두번째 방식으로 마이그레이션하자는 결정을 내렸다.

앞에서의 대기열을 Kafka로 선택했다. 각 사용자별로 토픽을 생성하고, 파티션을 하나로 가져가면서 순서를 보장하려는 계획을 세웠었는데 사용자로부터 특정 사용자의 순번과 남은 시간을 요청했을 때, 파티션의 offset 뒤에 있는 정보들을 가져와 처리할 수 없다는 문제가 있었다.

그에 따른 해결책으로 다시 Redis를 도입해 대기열 정보를 저장하려 했지만, 중복 저장 문제와 Kafka와 같은 트래픽을 받아야하는 처음 생각했던 문제로 돌아가게 되었다.

이에 따른 해결책으로 Event Sourcing과 CQRS를 사용한 해결책이다.
이벤트 스토어로 Kafka를 사용하게 된다. 대기열을 처음 생성하면 사용자 별로 대기열 토픽, 이벤트 토픽을 생성한다.
대기열을 추가하면서 대기열 추가 이벤트, 대기열에서 소모되면 작업 시작 이벤트, 작업이 끝나면서 작업 끝 이벤트로 각각의 토픽에 쌓이게 된다.
스케줄러가 주기적으로 replay를 통한 최신 대기열 정보를 스냅샷으로 제공해 각각의 사용자는 현재 순번을 확인할 수 있게 된다.

이에 따른 문제가 Bean 관리였다. 사용자별로 하나의 Producer, 2개의 Consumer가 필요하기 때문에 어떤 사용자가 어떤 Producer, Consumer를 사용하는지 Bean을 통해 구분해야 했기에 관리복잡도가 증가하는 문제가 생겼다.

이에 따른 해결책으로 순서를 보장하지만, 중복을 허용하는 Producer, 최종적 일관성을 제공하는 멱등성 Producer 같은 우리가 제공할 옵션에 따른 설정을 서버 시작 시에 미리 만들어놓고, 사용자별로 어떤 옵션을 사용하는지만 판단해서 해당 옵션을 사용하게 하는 방법으로 해결했다.
cf) 멱등성 : 동일한 요청을 여러번 수행해도 결과가 일관되게 유지되어야한다는 원칙

위에서 해결된 줄 알았지만, Race Condition이라는 문제가 남아있었다. 특정 토픽에서만 트래픽이 몰린다고 가정한다면 같은 환경에서 동작하는 다른 사용자들에 대해서도 느려지는 문제가 발생했다. Auto Scaling 을 한다고 하더라도 단 한 사용자에 대한 트래픽 때문에 서버 전체를 늘려야하는 문제가 있었다.

그에 따른 해결로 서버 자체를 따로 만들어주는 동적 인스턴스 생성 방식을 최종적으로 채택했다.
대기열 생성 요청이 들어오면 큐 관리서버에서 Docker out of Docker 방식으로 대기열을 처리하는 서버를 동적으로 띄워주고, 해당 메타정보를 Redis에 저장해 Gateway에서 동적 라우팅이 가능하게 할 것이며, 기존 Event Sourcing 방법을 재활용해서 큐 서버로부터 받아온 이벤트들로 백업본을 저장하는 용도로 사용할 예정이다. 이 방법 또한 큐 관리 서버의 자원을 소모해서 인스턴스를 띄워주기에 완벽하진 않다. 쿠버네티스 같은 컨테이너 오케스트레이션 도구를 통해 분리가 필요하지만, 짧은 기간상 여기까지 기획하고 개발에 들어갔다.

개발 과정에서 다시 대기열 구현으로 돌아와보면 Kafka에서 다시 Redis로 돌아옴에 따라 여러 자료구조를 분석해 짧은 시간복잡도를 가져가려 고민했다. 최종적으로 SortedSet을 통해 간단하게 구현하고, 성능테스트를 통해 한계점을 파악하고, 효율적인 구조로 변경할 예정이다.
최종 서비스 아키텍쳐 구조

추후계획(10/11~10/25)

시간이 남으면 쿠버네티스를 적용할 예정이다.
'프로젝트 > 스프링심화1기' 카테고리의 다른 글
Chapter 5. 팀 프로젝트 2주차 WIL (0) | 2024.10.07 |
---|---|
Domain Driven Design (DDD) (0) | 2024.09.09 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!