본문 바로가기
카테고리 없음

[우아한 티켓팅] 친절한 대기열 시스템 설계하기

by hseong 2024. 9. 24.

들어가기에 앞서

본 게시글은 제가 참여했던 팀 프로젝트 우아한 티켓팅에서 대기열 시스템을 설계했던 이야기를 다룹니다. 상세한 구현보다는 저희가 원하는 대기열을 만들기 위해 했던 다음과 같은 고민들을 남기려 합니다.


  1. 어디서 아이디어를 얻었는지
  2. 아이디어를 구현으로 옮기기 위해서 어떤 문제를 해결해야 했는지
  3. 짝(페어) 프로그래밍을 진행하며 객체 간 협력 구조를 어떻게 설계했는지

설계 목표

  • 우아한 티켓팅의 대기열 시스템은 새로 고침해도 사용자의 순서를 유지하는 친절한 대기열 시스템을 목표로 합니다.
  • 사용자의 실수, 지루함에 의한 새로고침 클릭 정도는 용인해주어 사용자 경험을 향상시킵니다.

아이디어

현실 세계에서 순서를 유지하는 대기열은 은행이나 식당 대기열에서 찾아볼 수 있었습니다. 예를 들어 은행의 대기 시스템은 다음과 같은 흐름으로 동작합니다.

  1. 사용자는 은행에 입장하여 번호표 발급기에서 번호표를 뽑습니다.
  2. 사용자는 대기 공간에서 대기합니다.
  3. 사용자는 은행원의 앞에 있는 빨간 번호 표시기의 숫자와 자신의 번호표를 비교해서 남은 순서를 파악합니다.
  4. 빨간 번호 표시기가 띵동 하고 울리면 은행원 앞에 앉아 작업을 시작합니다.

저희 시스템은 여기에서 아이디어를 얻었습니다. 마치 은행과 같이 1) 대기 번호 발급기, 2) 대기열, 3) 입장 번호 표시기, 4) 작업 공간 까지 네 가지 핵심 요소로 구성하였습니다.

이제 이러한 추상적인 설계를 구현으로 옮기기 위해서는 두 가지 중요한 문제를 해결해야 했습니다.

문제

하나. 언제 대기열의 사용자를 작업 공간으로 옮겨줄까?

가장 간단한 방법으로는 스케줄링을 이용하여 주기적으로 행위를 실행시키는 방법이 있었습니다. 하지만 스케줄링의 경우 사용자가 대기열에 없어도 불필요한 동작을 반복한다는 단점이 있었습니다. 저희는 이러한 불필요한 동작을 줄이고 싶었습니다.

불필요한 동작을 줄이려면 사용자가 '자신의 남은 순서를 물어봤을 때', 작업 공간에 자리가 있는지 확인하고 대기열에서 빼주는 방법을 생각할 수 있었습니다.

하지만 대기열 페이지에서 자기 순서를 물어보는 사용자는 수 천, 수 만명 일 것입니다. 그렇다면 대기열에서 작업 공간으로 옮겨주는 동작도 수천번 수행하게 되는 문제가 발생할 것이라고 예측할 수 있었습니다.

이를 해결하기 위해 고민하던 중 디바운스라는 키워드를 떠올렸습니다. 사용자의 요청을 모아뒀다가 한 번만 실행시키는 겁니다. 수천 개의 요청 중 오직 하나의 요청이 작업 공간의 빈 자리만큼 대기열의 사용자를 빼서 작업 공간으로 이동시키는 겁니다. 사용자의 실제 요청이 있을 때만 일정한 주기마다 작업이 실행되도록 만들어 첫 번째 문제를 해결할 수 있었습니다.

둘. 대기열에서 떠난 사용자와 활성 사용자를 어떻게 구분하지?

두번째 문제는 대기열 페이지의 속박을 벗어던지고 행복을 찾아 떠난 사용자입니다. 저희 대기열 시스템은 새로고침해도 순서를 유지하는 친절한 대기열입니다. 때문에 사용자가 중간에 자리를 비운 경우에도 사용자 정보를 유지하고 있습니다.

그런데 이 사용자가 완전히 떠나버린 경우에 문제가 발생합니다. 떠난 사용자가 작업 공간에 들어갈 차례가 되었고 실제로 들어가게 된다면 아무런 작업을 하지 않음에도 불필요하게 작업 공간을 점유하게 됩니다. 바로 뒤에서 기다리고 있는 사용자는 작업을 원하는 활성 사용자임에도 불구하고 불필요한 대기를 해야 합니다. 서버 자원 또한 낭비될 것입니다.

이를 해결하기 위해 생각한 방법은 작업 만료 시간에 있었습니다.

최초에 작업 공간의 빈자리를 대기열의 사용자로 채울 때, 사용자의 만료 시간은 30초 정도로 짧게 설정합니다. 그리고 사용자는 자신의 남은 순서를 알기 위해 마지막 요청을 보내는 순간이 옵니다. 그 때, 사용자의 대기 시간을 작업이 가능한 만큼 설정해주는 것으로 해결할 수 있었습니다.

짝 팀원이 말했다. "이해하기 어렵다."

초안

저희는 짝 프로그래밍을 통해 앞선 설계를 바탕으로 실제 구현을 위해 각 구성 요소가 어떻게 협력해야 할지 협력 구조를 설계했습니다. 패키지 구조, 역할에 따른 인터페이스, 협력을 명시적으로 표현하기 위한 추상 클래스를 만들어가며 서로 의견을 나눴습니다. 이러한 과정을 거쳐서 나온 협력 구조의 초안은 다음과 같습니다.

앞에서 설계 했던 것과 달리 대기 공간이라는 요소가 추가된 것을 볼 수 있습니다. 대기 공간은 대기열을 보조하기 위한 부가 요소입니다. 대기열을 구현하는 데 어떤 기술을 사용할지는 알 수 없지만 큐(Queue)와 유사한 자료 구조를 사용할 것임은 추측할 수 있었습니다. 이 때, 사용자가 대기열에 이미 입장 했는지 알기 위해서 필연적으로 큐를 탐색해야 할 것입니다. 이는 비효율적인 작업이라 생각했습니다. 따라서 대기 공간이라는 부가 요소를 뒀습니다. 대기열을 탐색하는 대신 맵(Map)을 이용해 사용자의 입장 여부를 바로 파악할 수 있게 하는 역할을 합니다.

각각의 구성 요소는 자신의 역할을 수행하기 위한 자료 구조를 하나씩 담당합니다. Redis를 예로 들면 다음과 같습니다.

  • 대기 번호 발급기, 입장 번호 표시기 - string
  • 대기열, 작업 공간 - sorted set
  • 대기 공간 - hash

짝 피드백

초안을 바탕으로 저는 Redis를 이용한 대기열을, 짝 팀원은 Java 자료 구조를 이용한 대기열을 담당했습니다. 각자 구현을 진행하고 하루가 지난 시점에 짝 팀원은 '현재의 구조가 이해하기 어렵다.'라는 의견을 전해왔습니다. 의견과 함께 새롭게 제시한 구조는 다음과 같습니다.

협력 구조 초안과 달리 계층형 구조를 사용하고, 구성 요소를 단순화하여 리포지터리에 몰아 넣은 것이 특징입니다. 짝 팀원의 의견에 따라 협력 구조를 바꾼다면 분명 구현하기 쉽고, 다른 사람에게 설명하기 쉬운 구조를 만들 수 있을 것입니다. 그러나 기존과 달리 모든 구성 요소를 Repository 하나에서 책임지게 되어 역할이 너무 커지는 문제가 발생합니다. 역할이 커지면 코드 길이가 길어지고 읽기도 힘들 것입니다. 따라서 이러한 의견을 짝 팀원에게 전달했습니다.

최종안

초안과 짝 제시안에 따라 상호 의견 조율을 거쳐서 나온 협력 구조의 최종안은 다음과 같습니다.

최종안은 짝 팀원의 의견에 따라 계층형 구조로 변경하였습니다. 명확히 계층을 나눈다면 좀 더 이해하기 쉬울 수 있다고 생각했습니다. 다만, 구성 요소는 기존과 동일하게 책임에 따라 인터페이스를 나누는 것으로 합의하였습니다. 이러한 상호 의견 조율 통해 좀 더 이해하기 쉬우면서 책임 분리도 고려한 협력 구조를 완성하였습니다.

다음으로

지금까지 우아한 티켓팅의 추상적인 설계와 협력 구조 설계 과정이었습니다. 저희는 이러한 설계를 바탕으로 Java 메모리 대기열, Redis 대기열을 구현하고 성능 비교를 진행하였습니다. 다음 편에서는 저희 팀이 어떻게 부하 테스트를 진행했는지, 어떤 것이 성능상에서 우위를 보였는지 다루겠습니다.