평소 초당 500건을 처리하던 서비스에 이벤트나 프로모션으로 초당 5,000건이 들어온다고 생각해 보겠습니다. 애플리케이션은 요청을 거절하지 않으려고 모두 메모리 큐에 쌓고, 작업 스레드를 늘리며, 실패한 요청을 즉시 재시도합니다. 처음 몇 분은 버티는 것처럼 보이지만 곧 응답 시간과 메모리가 급증하고 데이터베이스 연결 풀이 고갈됩니다. 결국 중요한 결제 요청까지 타임아웃됩니다.
과부하 상황의 핵심 문제는 서버 수가 부족하다는 사실만이 아닙니다. 유입 속도가 처리 속도보다 빠른데도 시스템이 이를 상류에 알리지 않고 계속 받아들이는 것입니다. 백프레셔(Backpressure)는 하류의 처리 여력이 부족할 때 유입을 늦추거나 멈추는 제어 방식이고, Load Shedding은 감당할 수 없는 일부 작업을 명시적으로 거절해 전체 시스템을 보호하는 전략입니다.
큐는 순간적인 변동을 흡수하는 좋은 도구지만 처리 능력을 새로 만들지는 않습니다. 이번 글에서는 큐가 언제 도움이 되고 언제 장애를 키우는지, 대기열 크기와 시간 제한, 재시도, 우선순위와 복구를 어떻게 설계해야 하는지 살펴보겠습니다.
과부하는 응답 시간이 느려지는 문제로 시작해 연쇄 장애로 끝난다
처리량이 한계에 가까워지면 응답 시간은 선형으로 증가하지 않습니다. CPU, 스레드, DB 연결과 잠금 같은 공유 자원이 포화되면서 작은 유입 증가가 긴 대기로 이어집니다. 호출자는 늦어진 응답을 실패로 판단해 재시도하고, 재시도 트래픽이 원래 요청에 더해져 하류를 더욱 압박합니다.
한 서비스의 지연은 상위 서비스의 작업 스레드와 연결을 붙잡습니다. 상위 서비스도 새 요청을 처리하지 못하고 그 위의 Gateway까지 정체됩니다. 의존성이 많은 마이크로서비스에서는 작은 하류 장애가 전체 서비스로 전파되는 이유입니다.
따라서 과부하 대응의 목표는 모든 요청을 어떻게든 성공시키는 것이 아닙니다. 시스템이 안전하게 처리할 수 있는 범위를 알고, 초과 수요에 빠르고 예측 가능한 결과를 주며, 부하가 내려간 뒤 신속하게 정상으로 돌아오는 것입니다.
유입률과 처리율의 차이가 대기열을 만든다
단순화하면 단위 시간당 들어오는 작업 수를 유입률 λ, 처리 가능한 작업 수를 μ라고 할 수 있습니다. 짧은 순간 λ가 μ보다 커도 큐가 여유를 흡수한 뒤 한가한 시간에 처리할 수 있습니다. 그러나 평균적으로 λ가 계속 μ보다 크면 큐는 언젠가 반드시 가득 찹니다.
Little의 법칙은 안정된 시스템에서 평균 대기 작업 수 L이 처리율 λ와 평균 체류 시간 W의 곱이라는 관계를 보여줍니다. 큐에 평균 10,000건이 있고 초당 100건을 처리한다면 단순 계산으로 작업이 시스템을 빠져나오는 데 약 100초 규모가 걸립니다. 큐 길이만 보고 “아직 저장 공간이 있다”고 안심하면 사용자가 체감하는 지연을 놓칩니다.

실제 시스템에서는 작업별 처리 시간이 다르고 외부 API 지연도 변합니다. 평균만으로 설계하지 말고 피크 유입, 처리 시간 분포, p95 큐 대기 시간과 가장 오래된 작업의 나이를 함께 측정해야 합니다.
무제한 큐는 버퍼가 아니라 장애 저장소가 된다
무제한 큐는 거절을 피하는 대신 메모리 고갈 시점을 늦춥니다. 작업 객체와 요청 본문, 재시도 정보가 쌓이면 GC 정지와 페이지 교체가 발생하고 정상 처리 속도까지 낮아집니다. 처리율이 떨어지면 큐가 더 빨리 쌓이는 악순환이 생깁니다.
디스크 기반 메시지 브로커라 해도 무한히 안전하지 않습니다. 저장 공간과 복제 네트워크, 소비 지연이 한계에 도달할 수 있고, 수시간 전의 가격 조회나 알림처럼 이미 가치가 사라진 작업을 계속 처리할 수도 있습니다. 오래된 작업이 새롭고 중요한 작업 앞을 막는 head-of-line blocking도 생깁니다.
큐는 반드시 용량이 제한되어야 하며 가득 찼을 때 정책이 있어야 합니다. 생산자를 기다리게 할지, 신규 작업을 거절할지, 오래된 작업을 버릴지, 낮은 우선순위를 제거할지는 업무 의미에 따라 결정합니다. 아무 정책도 정하지 않으면 런타임의 메모리 부족이 사실상 정책이 됩니다.
큐의 크기는 허용 지연에서 역산한다
큐를 “메모리에 백만 건 들어가니까 백만”으로 정하는 것은 위험합니다. 사용자가 5초 안에 결과를 받아야 하고 안정 처리율이 초당 200건이라면, 수십만 건의 큐는 이미 SLA를 지킬 수 없는 작업을 보관할 뿐입니다.
허용 가능한 큐 대기 시간과 처리율을 이용해 초기 용량을 계산하고, 작업 크기와 장애 시 복구 시간을 반영합니다. 실시간 요청은 매우 짧은 큐가 적합하고, 이메일 발송이나 영상 변환 같은 비동기 작업은 더 긴 지연을 허용할 수 있습니다.
하나의 공용 큐에 모든 작업을 섞지 않는 것도 중요합니다. 비밀번호 재설정 메일이 대량 마케팅 메일 뒤에서 한 시간 기다리면 안 됩니다. 업무 중요도와 지연 목표가 다른 작업은 별도 큐와 소비 용량을 갖는 편이 격리에 유리합니다.
백프레셔는 하류의 상태를 상류의 행동으로 바꾼다
동기 HTTP 요청에서는 동시 처리 수를 세마포어나 제한된 작업 풀로 제어하고, 여유가 없으면 짧게 대기한 뒤 429 또는 503과 Retry-After를 반환할 수 있습니다. 늦게 실패하는 것보다 빠르고 명확하게 거절하면 호출자가 자원을 오래 붙잡지 않습니다.
스트리밍과 메시지 처리에서는 소비자가 감당할 수 있는 만큼만 가져오거나 ack되지 않은 메시지 수를 제한합니다. 내부 파이프라인도 제한 채널을 사용하면 소비가 느려질 때 생산 작업이 자연스럽게 대기합니다. 생산자가 무조건 비동기로 쌓기만 하면 백프레셔가 끊깁니다.
백프레셔 신호는 가능하면 시스템 경계를 넘어 전달되어야 합니다. 하류가 2초밖에 남지 않은 요청을 받지 않도록 전체 요청의 deadline과 취소 신호를 전달합니다. 사용자가 이미 연결을 끊었는데 하류 서비스가 수십 초 동안 DB 작업을 계속하는 낭비를 줄일 수 있습니다.
타임아웃은 각 호출마다 새로 시작하는 스톱워치가 아니다
상위 요청에 3초 제한이 있는데 세 개의 하류 호출마다 3초 타임아웃을 주면 전체는 훨씬 길어질 수 있습니다. 요청이 시작할 때 전체 deadline을 정하고 네트워크와 큐 대기, 하류 처리에 남은 예산을 배분해야 합니다.
큐에 2.9초 기다린 요청은 실행을 시작해도 사용자에게 유효한 응답을 줄 가능성이 낮습니다. 작업의 deadline이나 최대 age를 확인해 실행 전 만료된 작업을 제거하면 귀중한 처리 용량을 새 요청에 사용할 수 있습니다.
타임아웃 값이 지나치게 짧으면 정상 작업을 취소하고 재시도를 늘리며, 너무 길면 자원을 붙잡습니다. 실제 지연 분포와 사용자 목표를 기준으로 계층별 예산을 정하고 의존 서비스보다 호출자의 제한이 약간 더 길어 응답을 정리할 시간을 확보합니다.
재시도는 작은 장애를 큰 트래픽으로 바꿀 수 있다
하류가 느려졌을 때 모든 호출자가 즉시 세 번 재시도하면 부하는 원래의 몇 배가 됩니다. 여러 계층이 각각 재시도하면 증폭은 더 커집니다. 재시도는 성공 가능성이 있는 일시적 오류에만, 가능한 한 한 계층에서 수행해야 합니다.
지수 백오프와 무작위 지연(jitter)을 사용해 요청이 같은 순간에 다시 몰리지 않게 하고, 전체 시간 예산과 최대 횟수를 제한합니다. 서버가 Retry-After를 제공하면 이를 존중합니다. 재시도 예산을 두어 전체 요청 중 일정 비율 이상이 재시도가 되지 않게 하는 방법도 효과적입니다.
쓰기 요청은 같은 작업을 다시 보내도 중복 결과가 생기지 않도록 멱등성 키와 상태 확인이 필요합니다. 그러나 멱등성은 부하를 없애지 않으므로 서버가 이미 처리한 키를 저렴하게 확인할 수 있어야 합니다.
Load Shedding은 실패를 선택해 성공을 지키는 전략이다
처리 능력을 넘어선 요청을 모두 받아 결국 100%가 느리게 실패하는 것보다 일부를 빠르게 거절하고 나머지를 정상 처리하는 편이 사용자 경험과 복구에 유리합니다. 이를 Load Shedding이라고 합니다.

정책은 단순 선착순만이 아닙니다. 인증된 고객과 익명 크롤러, 결제와 추천 갱신, 실시간 요청과 배치 작업의 중요도가 다를 수 있습니다. 이미 만료된 작업이나 중복된 최신 상태 갱신은 먼저 제거할 수 있습니다. 다만 고객 등급이나 지역별 우선순위는 공정성과 계약을 검토해 투명하게 설계해야 합니다.
거절 응답은 클라이언트가 어떻게 행동해야 하는지 알려줘야 합니다. 재시도 가능 여부와 예상 대기, 비동기 접수 대안이나 상태 조회 방법을 제공합니다. 화면이 무한 로딩되는 것보다 “요청이 많아 잠시 후 다시 시도해 주세요”라는 빠른 응답이 낫습니다.
단계적 기능 저하는 핵심 여정을 남긴다
부하가 높을 때 모든 기능을 같은 품질로 제공하려 하지 말고 비용이 큰 부가 기능부터 줄일 수 있습니다. 검색 결과의 개인화와 추천을 끄고 기본 정렬을 제공하거나, 실시간 통계를 짧은 캐시로 대체하고, 고해상도 이미지 변환을 나중으로 미룹니다.
어떤 기능을 어떤 순서로 줄일지 미리 정하고 기능 플래그나 운영 설정으로 제어합니다. 데이터 정합성이나 보안 검사를 생략하는 방식은 허용해서는 안 됩니다. 저하는 편의와 신선도를 낮추되 핵심 거래의 정확성은 유지해야 합니다.
대체 응답에도 표시가 필요할 수 있습니다. 오래된 캐시를 최신값처럼 보여주면 사용자가 잘못된 결정을 내릴 수 있습니다. 데이터 갱신 시각과 제한된 기능을 명확히 안내합니다.
Circuit Breaker와 Bulkhead로 장애 전파를 막는다
외부 API가 계속 실패하는데 모든 요청이 타임아웃까지 기다리면 연결 풀이 고갈됩니다. Circuit Breaker는 일정 기준 이상 실패하면 잠시 호출을 차단하고 빠르게 대체 응답을 반환합니다. 이후 일부 시험 요청으로 회복을 확인한 뒤 점진적으로 다시 엽니다.
Bulkhead는 선박의 격벽처럼 자원을 격리합니다. 외부 리포트 API용 연결과 핵심 결제 DB 연결을 분리하고, 테넌트나 작업 유형별 동시성 한도를 두면 한 영역의 과부하가 전체 자원을 소비하지 못합니다. 스레드 풀을 무조건 여러 개 만드는 것이 아니라 실제 병목 자원이 함께 격리되는지 확인해야 합니다.
자동 확장은 필요하지만 즉시 해결책은 아니다
인스턴스를 늘려 처리율을 높일 수 있지만 새 인스턴스가 준비되는 데 시간이 걸리고 데이터베이스와 외부 API는 그대로일 수 있습니다. 애플리케이션만 확장하면 DB 연결 수가 급증해 하류가 먼저 무너질 수도 있습니다.
CPU만으로 확장하면 I/O 대기와 큐 적체를 놓칩니다. 큐 길이, 가장 오래된 메시지의 나이, 처리율과 목표 소진 시간을 조합해 필요한 소비자 수를 계산하는 것이 좋습니다. 동시에 하류가 허용하는 최대 동시성을 상한으로 둬야 합니다.
부하가 줄었을 때 너무 빨리 축소하면 남은 큐를 처리하지 못하고 진동이 생깁니다. 확장과 축소의 서로 다른 임계값, 안정화 시간과 최소 용량을 설정합니다.
메시지 큐 운영에서 놓치기 쉬운 것
처리에 계속 실패하는 Poison Message가 앞에서 반복되면 파티션 전체가 멈출 수 있습니다. 제한된 재시도 뒤 DLQ로 격리하고 알림, 원인 수정과 재처리 절차를 마련해야 합니다. DLQ는 버리는 곳이 아니라 소유자와 보존 기간이 있는 운영 큐입니다.
순서 보장이 필요한 키는 같은 파티션으로 보내야 하지만 특정 인기 키에 트래픽이 몰리면 한 파티션만 병목이 됩니다. 전역 순서를 요구하기 전에 업무상 실제 필요한 범위를 줄이고, 병렬 처리 가능한 단위를 찾아야 합니다.
소비자가 메시지를 가져온 뒤 죽을 수 있으므로 최소 한 번 전달을 가정하고 중복 처리에 안전하게 설계합니다. 처리 완료와 ack 사이의 실패, DB 저장과 다음 메시지 발행 사이의 원자성도 테스트해야 합니다.
운영에서 봐야 할 지표
요청 수와 CPU만으로는 과부하의 방향을 알기 어렵습니다. 아래 지표를 서비스와 작업 유형별로 관찰합니다.
- 유입률과 성공 처리율의 차이
- 큐 길이와 가장 오래된 작업의 나이
- 큐 대기 시간과 실제 처리 시간의 분포
- 동시 작업 수, 스레드·연결 풀 대기와 포화도
- 429·503과 Load Shedding 비율
- 재시도율, Circuit Breaker 상태와 DLQ 유입
- 작업 만료·취소·중복 제거 건수
- 부하가 정상화된 뒤 큐를 비우는 데 걸리는 시간
큐 길이만 알림으로 쓰면 소비 속도가 매우 빠른 정상 피크에도 경보가 울립니다. 작업 나이와 증가 속도, SLO 영향을 함께 사용하면 실제 사용자 위험을 더 잘 반영합니다.
부하 테스트는 최고 처리량보다 무너지는 방식을 확인한다
일정한 트래픽만 올려 최대 TPS를 측정하는 것으로는 충분하지 않습니다. 순간 폭주, 느린 외부 API, DB 연결 감소, 소비자 일부 종료, Poison Message와 재시도 폭풍을 주입해야 합니다.
확인할 것은 피크 숫자뿐 아니라 한계에 도달했을 때 빠르게 거절하는지, 핵심 요청이 계속 성공하는지, 메모리가 제한 안에 있는지, 부하가 끝난 뒤 자동으로 회복하는지입니다. 테스트 클라이언트도 백오프와 실제 연결 패턴을 반영해야 현실적인 결과를 얻습니다.
장애 후 큐가 남아 있는 상태에서 새 실시간 요청과 백로그를 어떤 비율로 처리할지도 검증합니다. 백로그만 빠르게 비우려다 새 요청이 계속 늦어지면 서비스는 회복된 것처럼 보이지 않습니다.
작게 시작하는 과부하 방어 순서
- 핵심 경로의 처리율과 지연 목표를 측정합니다. 병목 자원과 안전한 동시 처리 수를 찾습니다.
- 모든 내부 큐와 작업 풀에 상한을 둡니다. 가득 찼을 때의 대기·거절·폐기 정책을 명시합니다.
- 전체 deadline과 취소를 전파합니다. 만료된 작업이 자원을 소비하지 않게 합니다.
- 재시도에 백오프, jitter와 예산을 적용합니다. 쓰기는 중복 처리에 안전하게 만듭니다.
- 핵심과 부가 작업을 격리합니다. 우선순위 큐, Bulkhead와 단계적 기능 저하를 준비합니다.
- 큐 나이와 거절률을 관측합니다. 과부하 시작과 복구 시간을 대시보드에서 확인합니다.
- 폭주와 의존성 장애를 훈련합니다. 자동 확장과 차단 정책이 함께 작동하는지 검증합니다.
안정적인 시스템은 모든 요청을 받지 않는다
과부하에 강한 시스템은 무한한 자원을 가진 시스템이 아닙니다. 자신의 처리 한계를 알고, 수요가 한계를 넘을 때 이를 상류에 전달하며, 가치가 낮거나 이미 늦은 작업을 명시적으로 줄일 수 있는 시스템입니다.
핵심 원칙은 간단합니다. 큐는 제한하고, 시간 예산은 끝까지 전달하며, 재시도는 통제하고, 핵심 기능을 위해 일부 부하를 빠르게 거절하라. 이렇게 설계하면 트래픽 폭주는 전체 장애가 아니라 예측 가능한 품질 저하로 관리할 수 있습니다.