대용량 트래픽 대응 순서: 스레드풀부터 캐시와 확장까지
대용량 트래픽은 단순히 사용자가 많다는 뜻이 아니다. 짧은 시간에 요청이 몰리면서 서버가 처리할 수 있는 동시 작업 수를 넘어서는 상황을 말한다. 요청은 스레드풀에 배정되고, 모든 스레드가 사용 중이면 큐에서 대기한다. 큐가 계속 길어지면 응답 시간이 늘고 결국 타임아웃과 재시도가 또 다른 부하를 만든다.
먼저 확인할 지표
스케일 아웃부터 시작하면 비용만 늘고 병목은 그대로일 수 있다. 다음 지표를 같은 시간 구간으로 모은다.
- 초당 요청 수와 엔드포인트별 분포
- p50, p95, p99 응답 시간
- HTTP 오류율과 타임아웃 수
- 웹 서버 스레드풀의 active, queued, rejected 수
- CPU, 메모리, GC, 커넥션 풀 대기 시간
- 데이터베이스 쿼리 시간과 느린 쿼리 수
평균 응답 시간만 보면 일부 사용자의 긴 대기 시간을 놓친다. 특히 재시도 정책이 있는 클라이언트는 실제 유입량보다 많은 요청을 만들 수 있으므로 원 요청과 재시도 요청을 구분한다.
1단계: 요청을 줄이고 빠르게 실패시키기
서버가 처리할 수 없는 요청을 모두 붙잡고 있으면 정상 요청까지 느려진다. 요청 본문 크기와 파일 크기를 제한하고, 인증·권한 검사를 비싼 조회보다 앞에 둔다. 외부 API 호출에는 명시적인 타임아웃과 제한된 재시도 횟수를 둔다. 큐의 최대 길이와 대기 시간을 정하고 초과하면 429 또는 503으로 빠르게 응답하는 편이 장애 전파를 막는다.
스레드풀 크기를 무작정 늘리는 것도 위험하다. 스레드가 늘어도 DB 커넥션이 부족하면 대기만 길어지고 메모리와 컨텍스트 스위칭 비용이 커진다. 애플리케이션 스레드, DB 커넥션, 외부 API 동시 호출 수를 하나의 예산처럼 본다.
2단계: 반복 조회를 캐시로 분리하기
변경이 드물고 여러 사용자가 함께 읽는 데이터는 캐시로 DB 부하를 줄일 수 있다.
요청
└─ 캐시 hit → 즉시 응답
└─ cache miss → DB 조회 → 짧은 TTL로 저장 → 응답
캐시를 적용할 때는 TTL, 무효화 시점, 오래된 값 허용 범위를 함께 정한다. 캐시가 만료되는 순간 같은 키로 요청이 몰리는 cache stampede를 막으려면 단일 조회자 락이나 TTL에 작은 변동을 둘 수 있다. 사용자별 권한이 섞인 결과를 공용 캐시에 저장하지 않도록 키 설계도 확인한다.
3단계: 수평 확장과 세션 상태 정리
한 서버의 CPU·메모리를 키우는 scale-up은 빠른 임시 대응이 될 수 있다. 하지만 단일 서버의 장애 위험과 상한은 그대로다. 여러 인스턴스를 두는 scale-out은 트래픽을 나눌 수 있지만, 로드 밸런서와 상태 공유가 필요하다.
세션을 서버 메모리에만 두면 요청이 다른 인스턴스로 이동할 때 로그인 상태가 사라질 수 있다. Redis 같은 공유 저장소에 세션을 두거나, stateless 토큰으로 전환하거나, 임시로 sticky session을 사용할 수 있다. sticky session은 특정 인스턴스에 트래픽이 고정되어 장애 격리와 균등 분산에 불리하므로 장기 해법으로는 신중해야 한다.
4단계: 데이터베이스의 읽기와 쓰기 분리
애플리케이션 서버를 늘려도 모든 요청이 하나의 DB로 향하면 DB가 마지막 병목이 된다. 읽기 트래픽이 많은 경우 복제본과 라우팅을 검토할 수 있지만, 복제 지연과 읽기 직후 최신성 문제를 먼저 정의해야 한다. 재고 차감이나 결제처럼 강한 일관성이 필요한 경로를 복제본으로 보내면 안 된다.
장애가 커지기 전의 보호선
서킷 브레이커, rate limit, 백프레셔, 우선순위 큐는 각각 다른 병목을 다룬다. 모든 기능에 한꺼번에 적용하기보다 실제 지표로 가장 먼저 무너지는 의존성을 찾아 한 곳씩 추가한다. 부하 테스트는 정상 응답만 확인하지 말고, 제한에 걸렸을 때의 상태 코드와 복구 시간도 기록한다.
대용량 트래픽 대응은 서버를 여러 대 복제하는 한 번의 작업이 아니다. 요청을 줄이고, 반복 조회를 캐시하고, 상태를 분리하고, DB의 최신성 기준을 정하는 순서로 진행해야 비용과 복잡도를 통제할 수 있다.
함께 읽기:
원문 기록: 대용량 트래픽 발생 시 어떻게 대응해야 하나요