우리 팀은 일반적인 MVC 구조로 수강신청 서비스를 개발했다.
사용자는 로그인한 뒤, 바로 수강신청 API를 호출하는 방식이었다.
처음엔 잘 작동했지만 JMeter 테스트에서 수천명이 동시에 접속할때 CPU와 DB 커넥션 풀이 빠르게 고갈되었고 서버는 병목 상태에 빠져 오류율이 컸다.
💡가장 먼저 든 생각은 “일단 한 번에 들어오지 않도록 막자”였다.
단순하게 생각하면, 입장할 수 있는 인원을 정해두고 그 외의 사람은 기다리게 하면 된다.
그렇다면 누가 기다려야 하고 누가 들어올 수 있는지 판단하는 서버가 따로 필요해진다.
🔽 그래서 수강신청 서버와는 별도로 대기열 서버를 구성하기로 했다.
기능을 나누면 아래와 같다.
💡그런데 한가지 의문점이 들었다.
그럼 입장 가능하다는 건 사용자에게 어떻게 알려주지❓
클라이언트가 아래와 같이 대기열 시스템에 일정한 주기로 계속 물어보면 되지 않을까?
“지금 들어가도 되나요?”
"지금은요?”
그게 바로 Polling 방식이었다.
polling 방식 적용 후 아키텍처는 위와 같이 바뀌었다.
자세한 로직 🔽
1. 클라이언트 대기열 진입
클라이언트는 POST /join API를 통해 대기열에 진입한다.
서버는 토큰 기반으로 사용자 정보를 생성해 큐에 저장한다.
사용자에게 현재 대기 순번을 응답한다.
2. 상태 확인 (Polling)
클라이언트는 GET /{uuid} API를 일정 주기로 호출해 자신의 상태를 확인한다.
서버는 WAITING 또는 ALLOWED 상태와 남은 순번을 반환한다.
클라이언트는 상태가 ALLOWED가 될 때까지 polling을 계속한다.
3. 빈자리 발생 시 수강신청 서버 → 대기열 서버 알림
수강신청 서버는 빈자리를 감지하면 POST /notify API를 호출해서
앞 순서 n명의 사용자에게 입장 허용 요청을 보낸다.
대기열 서버는 해당 인원을 ALLOWED 상태로 바꾼다.
4. ALLOWED 사용자 로그인
ALLOWED 상태가 된 사용자만 수강신청 서버에 로그인할 수 있다.
로그인 시 해당 사용자는 큐에서 제거된다.
| 구성 요소 | 설명 |
|---|---|
QueueManager | 대기열 큐 및 순번 계산 관리 |
queue | 전체 대기열을 저장하는 큐 (Queue<QueueUser>) |
userMap | 사용자 토큰으로 빠르게 조회하기 위한 맵 |
globalIndex | 전체 순번 계산을 위한 전역 카운터 |
BatchManager | ALLOWED 상태 사용자 그룹 및 타이머 관리 |
batchTokenMap | 배치 단위 사용자 토큰 목록 |
batchFutures | 로그인 완료 여부 추적용 CompletableFuture 객체 |
| API | 설명 |
|---|---|
POST /join | 대기열 진입. 토큰 기반 사용자 생성 후 큐 등록 |
GET /{uuid} | 사용자 상태 확인 (WAITING or ALLOWED) 및 현재 순번 확인 |
POST /notify | 수강신청 서버가 빈자리 발생 시 앞 사용자 n명 ALLOWED 처리 요청 |
GET /clear | 테스트용 전체 대기열 초기화 |
우리는 100명부터 10,000명까지 다양한 시나리오를 구성해 총 4가지 상황에서 부하 테스트를 진행했다.
📌 각 시나리오에 대한 상세 설명은 아래 글에 정리되어 있다.
🔗 시나리오 설명 보러가기
대기열 시스템 도입과 Polling 방식을 적용한 현재 아키텍처는 이전 구조와 비교했을 때 오류율이 눈에 띄게 감소했다.
동시 5,000명 접속 시나리오에서의 변화를 예로 들어보겠다.
📉 MVP 초기 구조 vs Polling 도입 후 오류율
시나리오1
시나리오2
시나리오3
시나리오4
위 결과를 보면
ALLOWED가 될 때까지 계속 요청하기 때문에 표본 수가 많은것을 볼 수 있고 그 결과 정상적으로 많은 요청을 처리할 수 있는것으로 판단된다.😲 평균적으로 약 80% 이상 오류율이 감소했다.
대기열 도입과 polling 적용 후 안정성 올라갔다는 걸 수치로 확인할 수 있었고 그 수치는 엄청난 차이였다.
우리 팀은 처음부터 대기열 시스템 도입과 polling을 쓰려고 했던게 아니다.
수강신청 서버가 트래픽을 감당하지 못하고 터지는 걸 경험하면서
❌ 물론 완벽한 구조는 아니다.
서버가 반복적으로 요청을 받아야 하기에 리소스가 소모되고 실시간성도 다소 부족하다.
그래서 이 방법말고 회의를 통해 나온 다른 후보의 방법들도 사용해보려고 한다.