· 7분 읽기
트래픽 성장 단계별 스케일링 전략: 스파이크 대응부터 WebSocket 확장, 부하 검증까지
서비스가 커질수록 스케일링은 한 번의 큰 결정이 아니라 병목이 바뀔 때마다 반복하는 작업이 됩니다. 어떤 단계에서 무엇을 먼저 확장할지 판단하고, 순간적인 쓰기 폭주에는 어떻게 대응하며, 연결 상태를 유지해야 하는 WebSocket 서버는 어떻게 수평으로 늘리고, 그 결과를 어떻게 부하 테스트로 검증할지까지 하나의 흐름으로 정리합니다. 이 글은 단계별 스케일링 의사결정에서 시작해 스파이크 트래픽 대응, 실시간 서버 확장, 마지막으로 Locust 기반 검증까지 이어지는 확장 플레이북입니다.
단계별 스케일링 의사결정
스케일링은 미리 큰 구조를 만드는 것이 아니라 병목이 바뀔 때마다 계층적으로 확장하는 작업입니다. 단계별 우선순위는 다음과 같습니다.
초기: 단일 서버와 관리형 DB로 시작하고, 아키텍처를 키우기 전에 로그와 메트릭 같은 관측 체계부터 구축합니다.
트래픽 증가: 앱 서버를 수평으로 확장하고 로드밸런서를 두며, 정적 자산은 CDN으로 분리합니다.
데이터 병목: 읽기 캐시를 먼저 적용하고, read replica를 추가하며, 필요할 때만 샤딩·파티셔닝으로 넘어갑니다.
글로벌 규모: 멀티 리전으로 확장하고, 장애 격리를 위해 cell 기반으로 분리합니다.
의사결정 시점마다 다음을 확인해야 합니다. 현재 병목이 CPU·네트워크·DB 중 어디인지, 확장 후 늘어날 운영 복잡도를 팀이 감당할 수 있는지, 롤백 경로가 있는지입니다. 사용자 수만 보고 과도한 분산 구조를 도입하거나, 일관성 전략 없이 캐시부터 확대하거나, 확장성만 우선해 장애 반경(blast radius)을 키우는 것은 흔한 실수입니다. 좋은 스케일링 전략은 단계별 병목에 맞는 최소 구조를 선택하는 것이며, 관측 지표 없이 아키텍처만 키우면 비용과 복잡도만 늘어납니다.
스파이크 트래픽 대응: 큐·입장 제어·멱등성
단계별 확장이 점진적인 병목 대응이라면, 티켓팅이나 플래시세일처럼 순간적으로 쓰기가 폭주하는 상황은 별도의 전략이 필요합니다. 스파이크 트래픽은 읽기보다 쓰기 병목을 만들고, 핵심 리스크는 DB 락 경합과 커넥션 고갈입니다. 우선 적용할 세 가지는 다음과 같습니다.
1) 동기 처리를 큐 기반 비동기로 전환
API는 즉시 "접수"만 반환하고 실제 처리는 worker가 순차적으로 처리합니다. DB에 직접 몰리는 순간 쓰기를 이 방식으로 완화합니다.
2) 사용자 입장 제어
초당 허용량을 제한하고, 대기 순번을 제공하며, 클라이언트의 폴링 주기를 통제합니다. 사용자 체감과 시스템 보호를 동시에 잡는 데 가장 효과적인 방법입니다.
3) 멱등성 보장
같은 요청이 재시도되어도 결과가 한 번만 반영되도록 키를 설계해야 합니다. request_id나 user_id + event_id 조합이 대표적입니다.
자주 하는 실수는 재시도 정책만 넣고 멱등성 키를 두지 않는 것, 큐 길이나 처리량 모니터링 없이 worker만 증설하는 것, 실패 시 보상·환불 경로를 정의하지 않는 것입니다. 스파이크 대응의 본질은 처리량 확장이 아니라 쓰기 경합 제어이며, 큐·입장 제어·멱등성 세 축을 먼저 고정하면 장애 확률이 크게 줄어듭니다.
상태를 가진 실시간 서버 확장: WebSocket 세션 라우팅과 복구
앱 서버는 무상태(stateless)라 수평 확장이 비교적 단순하지만, WebSocket 서버는 연결 상태를 유지해야 하므로 다른 접근이 필요합니다. 서버를 여러 대로 늘리면 어떤 유저가 어느 서버에 연결되어 있는지가 분산되고, 이를 중앙에서 일관되게 관리하지 않으면 메시지 유실과 오배송이 발생합니다.
권장 구성
- Load Balancer: 연결을 분산합니다.
- WS 서버 N대: 실제 연결을 유지합니다.
- Redis Hash:
userId -> serverId형태로 세션 위치를 저장합니다. - Redis Pub/Sub: 서버 간 실시간 이벤트를 전달합니다.
- Redis Streams: 재연결 시 누락된 메시지를 복구합니다.
데이터 모델은 다음과 같습니다.
HSET ws:sessions {userId} {serverId}
PUBLISH ws:server:{serverId} {event-json}
XADD ws:events * userId {userId} payload {json}
핵심 흐름은 연결 성공 시 ws:sessions에 세션을 등록하고, A 유저가 B 유저에게 메시지를 보내면 B의 serverId를 Hash에서 조회한 뒤 해당 서버 채널로 Pub/Sub을 발행하고, 수신 서버가 로컬 소켓으로 전달하면서 이벤트를 Streams에 기록해 재전송 가능한 상태를 유지하는 순서입니다.
async function onConnect(userId: string, serverId: string) {
await redis.hset('ws:sessions', userId, serverId);
await redis.expire(`ws:alive:${userId}`, 30);
}
async function routeMessage(toUserId: string, payload: object) {
const targetServer = await redis.hget('ws:sessions', toUserId);
if (!targetServer) return;
await redis.publish(`ws:server:${targetServer}`, JSON.stringify({ toUserId, payload }));
await redis.xadd('ws:events', '*', 'userId', toUserId, 'payload', JSON.stringify(payload));
}
서버 종료나 장애에 대비해서는 Heartbeat와 TTL로 좀비 세션을 자동 정리하고, Graceful Shutdown 시 해당 서버의 세션을 일괄 제거하며, 클라이언트가 마지막 이벤트 ID를 보내면 Streams에서 누락분을 재전송하는 Reconnect Resume을 갖춰야 합니다. 관측 지표로는 연결 수·활성 세션 수, Pub/Sub 전달 지연(p95, p99), reconnect 후 누락 복구 성공률, 서버별 fan-out 처리량을 확인합니다. 확장형 WebSocket의 본질은 소켓 핸들링이 아니라 상태 일관성이며, 세션 위치·라우팅·복구 경로를 분리해 설계해야 장애 시 복구가 가능합니다.
부하 테스트로 검증하기: Locust 계층화 전략
지금까지의 스케일링 결정과 확장 설계가 실제로 버티는지는 재현 가능한 부하 테스트로 확인해야 합니다. Locust를 쓰는 이유는 "큰 부하"보다 "재현 가능한 시나리오"입니다. 환경별로 같은 시나리오를 돌려 회귀를 빨리 찾는 데 초점을 둡니다.
테스트는 목적에 따라 계층을 나눠 구성합니다.
smoke: 배포 직후 핵심 기능을 확인합니다.functional: CRUD와 비즈니스 흐름을 검증합니다.performance: 목표 TPS와 latency를 검증합니다.stress: 한계점을 탐색합니다.
최소 구조는 다음과 같습니다.
e2e/
config/{local,int,stage,prod}.yaml
suites/{smoke,functional,performance,stress}.py
locustfile.py
실행은 다음처럼 합니다.
locust -f suites/smoke.py --headless -u 20 -r 5 -t 2m --host https://api-int.example.com
운영 규칙으로는 시나리오 데이터를 테스트마다 독립적으로 생성하고, 테스트 종료 시 정리(cleanup)를 보장하며, CI에서 환경별 실행 시간 상한을 설정해야 합니다. 자주 하는 실수는 smoke와 performance를 구분하지 않아 CI가 느려지는 것, 테스트 데이터가 충돌해 flaky가 발생하는 것, 성공 기준(p95, 에러율) 없이 수치만 출력하는 것입니다. Locust 자동화의 핵심은 도구가 아니라 테스트 계층화이며, smoke·functional·performance를 분리하면 빠른 검증과 깊은 검증을 동시에 가져갈 수 있습니다.
정리
스케일링 전략은 단계별로 최소 구조를 선택하는 판단력, 스파이크 트래픽에서는 큐·입장 제어·멱등성이라는 세 축, 상태를 가진 WebSocket 서버에서는 세션 라우팅과 복구 설계, 그리고 이 모든 결정을 검증하는 계층화된 부하 테스트로 완성됩니다. 어느 하나만으로는 충분하지 않으며, 관측 지표를 기반으로 병목을 확인하고 필요한 만큼만 확장한 뒤 부하 테스트로 검증하는 순환이 결국 안정적인 트래픽 성장을 만듭니다.