다수의 유저들을 선착순으로 줄세워보자

yeezze·2022년 12월 9일
2

form 프로젝트

목록 보기
2/7

가장 중요한 포인트를 먼저 잡아보자

선착순 당첨 프로그램에서 동시에 들어오는 다수의 유저들을 어떻게 관리할 수 있을까요?
가장 먼저 구현 포인트를 이렇게 잡았습니다.

유저가 들어오면 (ex 10000명 신청, 100명 당첨)

  1. 무조건 대기열에 줄세운다.
  2. 기다리는 유저들이 화나지 않도록 실시간으로 대기 번호를 알려준다.
  3. 유저마다 선착순 실패 성공 여부 체크


우리 서비스에 동시에 찾아온 엄청나게 많은 고객들을 줄세우지 않고 문을 열어준다면 이렇게 되겠죠? 😅


줄을 세우고 순서대로 입장시킨다면 아주 보기 좋은 모습이 될거에요 🤗
생각해보면 실생활에서도 그렇습니다.
퇴근길의 버스 대기줄, 놀이기구 대기줄, 맛집 대기줄.. (요즘은 테이블링을 많이 쓰긴 하지만)

그렇기 때문에 다수의 유저가 동시에 접속하는 서비스들을 생각해보면 대부분 대기순번을 알려주는 페이지가 존재합니다.
한번 쯤은 경험해본 적이 있을거에요. (수강신청, 예매 사이트, 선착순 쿠폰 이벤트 등)

그래서 저의 프로젝트에서도 무 조 건 대기열에 줄세운다!! 라는 규칙을 세우게 됐습니다.

대기열 구현 방법 등은 글의 하단에 있는 유튜브 영상을 참고했습니다.

flow

본격적인 개발을 하기 전에 큰 틀에서의 로직 흐름부터 작성해보고자 했습니다.


클라이언트가 웹을 통해 선착순 폼에 참가하여 제출하면 서버의 Answer API를 호출하게 됩니다.
해당 API는 SubmissionService라는 클래스의 addQueue()를 호출하여 대기열에 줄을 서는 로직을 실행합니다.

SubmissionService의 내부 로직 흐름입니다.
새로운 유저의 응답이 들어오면 addQueue() 메소드를 통해 Redis의 대기열에 줄을 세웁니다. 실패, 성공 여부에 따라 나뉘어져 성공한 유저는 enter() 메소드를 실행하게 되고, 실패한 유저는 대기번호를 받게 됩니다.

enter() 메소드에서는 입장 시간 순으로 정렬되어 있는 대기열의 유저 데이터들에 대해서 ZRANGE 명령을 통해 순서대로 처리해줍니다.
getOrder() 메소드에서는 ZRANK 명령을 통해 현재 유저의 대기열 순번을 안내합니다.

대기열에서 참가열로 이동하는 작업, 대기 번호를 새로 고침해주는 작업은 scheduler를 활용하여 처리하고자 했습니다.

고민 이슈

플로우 차트를 작성하고 개발을 진행하다 보니 2가지의 이슈가 떠올랐습니다.

1. 여러 이벤트가 동시에 일어나면 어떻게 관리할 것인가?

맨 처음에 작성했던 코드는 이런식으로 구현을 했습니다.

    Set<Event> events = eventFactory.events();
    for(Event event : events) {
    		// eventId를 통해 redis 내부 다수의 대기열에서 해당되는 대기열을 찾고
            
            for(User user : waittingQueue){
            	// 찾은 이벤트 대기열에 줄서있는 다수의 유저들을 for문으로 돌면서,
	            // 순서대로 입장 처리 및 대기 순번 새로 고침을 해준다.
            }
    }

진행 중인 여러 이벤트들을 for문으로 처리하고 그 안에서 이벤트별로 대기 중인 다수의 유저들을 다시한번 순서대로 처리해주는 로직이었습니다.

위처럼 로직을 구현하고 만약 네이버에서 진행하는 이벤트와 카카오에서 진행하는 이벤트가 1월1일 14시에 동시에 오픈된다면 어떻게 되는거지? 순서대로 진행하면 안될 것 같은데... 하는 생각이 들면서 고민이 생겼습니다.
14시 동시에 오픈했지만 두개의 이벤트가 순차적으로 진행되기 때문에 어떤 이유로 운 좋게 먼저 시작한 이벤트는 처리 되고, 다른 이벤트는 앞의 이벤트 처리가 끝날 때까지 아무것도 못하고 기다려야만 합니다.

다수의 이벤트가 순차적 처리가 아닌 병렬처리가 필요하겠다는 생각이 들었습니다.

병렬 처리 (parallelStream 활용)

eventFactory.events().parallelStream()
	.forEach(event -> {
      // eventId를 통해 redis 내부 다수의 대기열에서 해당되는 대기열을 찾고
      
      for(User user : waittingQueue){
	    // 찾은 이벤트 대기열에 줄서있는 다수의 유저들을 for문으로 돌면서,
		// 순서대로 입장 처리 및 대기 순번 새로 고침을 해준다.
      }
	}

바깥쪽 for문을 parallelStream을 활용하는 것으로 변경했습니다.
전체 유저를 처리하는 내부 반복문은 그대로 유지하고 이벤트를 처리하던 바깥쪽 반복문을 순차처리에서 병렬처리로 변경했습니다.
(병렬처리 적용과 관련한 내용은 해당 시리즈에 별도로 글을 작성할 예정입니다.)

2. 하나의 이벤트 안에서 다수의 유저들을 어떻게 관리할 것인가?

위의 flow에서도 잠깐 언급했지만 이벤트 내의 유저 관리에 관해서는 스케쥴러를 활용했습니다.

  1. 웹을 통해 선착순 응답 제출을 하면 서버 내부에서 유저는 대기열에 줄을 섭니다.
  2. 스케쥴러를 통해 1초에 한번씩 대기열 -> 참가열 이동 작업, 남은 모든 유저들에게 대기순번 새로고침하여 안내 작업을 실행합니다.
    (스케쥴러로 실행되는 로직과 무관하게 새로운 유저들은 계속 대기열에 새로 줄을 설 수 있습니다.)

구현

위의 이슈들을 고민하여 구현하니 이런 식으로 구현을 하게 됐습니다.
(로직 흐름 확인을 위해 간단하게 로그만 찍어봤습니다.)

대기열에 줄을 섭니다.

대기열에 줄을 선 순서대로 응답 제출을 처리합니다.

1초에 한번씩 기다리고 있는 모든 유저들에게 대기번호를 새로고침하여 안내해줍니다.




1초에 한번씩 응답 제출, 대기번호 새로고침


남은 갯수가 0개가 되면 선착순이 마감됩니다

다수의 이벤트가 동시에 진행되는 경우 병렬처리


2개의 이벤트가 순차처리가 아닌 병렬처리로 진행되는 것을 확인할 수 있습니다.

참고

https://youtu.be/MTSn93rNPPE
https://jupiny.com/2020/03/28/redis-sorted-set/

profile
백엔드 개발자 😊

2개의 댓글

comment-user-thumbnail
2024년 4월 15일

혹시 소슼코드 있을까요?

답글 달기
comment-user-thumbnail
2024년 4월 15일

혹시 소슼코드 있을까요?

답글 달기