🎧 ▶ 5분 브리핑으로 듣기
▶ 오디오북 재생 (Google Drive)
NotebookLM 오디오 개요 (AI 생성)
📕 전자책 전문 읽기 무너지지 않는 기술 · 34쪽
브라우저가 PDF를 바로 표시하지 못합니다. 새 탭에서 열기
이 글은 요약해 다시 쓴 것이고, PDF가 전문입니다.

1인 개발자로 프로덕션을 돌리고 그 서비스가 LLM API를 부른다면, 이 글은 트래픽 폭주와 비용 폭증이 동시에 덮쳐 올 때 서비스를 살리는 최소한의 규율을 알려 드립니다. 결론부터 말씀드립니다. 과부하에서 서비스를 무너뜨리는 건 외부 트래픽이 아닙니다. 시스템 스스로 부른 재시도 폭풍이 핵심입니다. 그리고 그 폭풍을 막는 유일한 길은, 무너지기 전에 미리 지금은 안 돼, 조금 후에 와라고 말해 두는 일입니다.

대부분의 개발자는 용량이 부족해서 죽는다고 생각합니다. 그래서 트래픽이 오면 서버를 늘리고 캐시를 붙이고 코어를 올립니다. 그 직감은 대개 옳습니다. 다만 재시도 폭풍은 다른 동물입니다. 이 글은 과부하가 어떤 순환으로 치닫는지를 먼저 짚고, 서버와 클라이언트와 LLM API라는 세 지점에서 그 순환을 끊는 방법을 숫자를 대며 확인합니다.

읽고 나면 토큰 버킷의 한도를 어떻게 정할지, 재시도에 왜 지터를 붙여야 하는지, LLM 예산을 왜 대시보드가 아니라 호출 직전의 코드에서 집행해야 하는지 스스로 판단할 수 있습니다. 이 세 가지는 전부 사고가 나기 전에 한도를 정해 두는 일이며, 합치면 과부하 대응의 전부입니다.

서비스를 무너뜨리는 건 외부 트래픽이 아니라, 내 재시도 폭풍이다 개념을 형상화한 이미지 글의 핵심 개념을 형상화했습니다.

서비스가 무너지는 순환: 재시도가 부르는 두 번째 물결

과부하가 진행되는 과정을 순서대로 적어 보면, 처음에는 용량보다 조금 많은 요청이 들어옵니다. 몇몇 요청이 대기하기 시작하고 하나를 처리하는 데 걸리는 시간이 늘어납니다. 처리가 느려진 만큼 동시에 쌓이는 요청이 더 많아집니다. 클라이언트와 로드 밸런서가 답을 기다리다 결국 타임아웃이 발생합니다. 그리고 타임아웃은 재시도로 바뀝니다. 트래픽이 다시 늘어납니다. 이 증가는 처리 지연을 다시 키우고 순환은 대기로 돌아옵니다.

여기서 치명적인 지점은 재시도입니다. 한 번의 실패가 재시도를 통해 두 번째 트래픽 물결로 돌아옵니다. 시스템은 원래 들어온 부하가 아니라, 자기가 부른 부하에 죽는 경우가 대부분입니다. 이 순환을 캐스케이드 실패라고 부릅니다. 상한이 없는 서비스라면 규모나 스택과 무관하게 대부분 여기서 끝납니다. 죽은 원인이 트래픽이 빨리 들어온 것은 아닙니다. 시스템 안 어디에서도 지금은 안 돼라고 말한 존재가 없었던 것입니다.

숫자를 넣어 보겠습니다. 1,000개의 클라이언트가 흔들리는 제공자를 부르고 있고, 처음에 500개가 실패했다고 가정하겠습니다. 이 500개가 모두 같은 순간에 재시도하면, 제공자는 남은 500개의 정상 요청에 재시도 500개를 더해 같은 간격에 1,000개를 받습니다. 또 실패하면 그 다음 간격에 1,000개의 재시도가 돌아옵니다. 부하는 매 라운드 두 배씩 불어납니다. 원래 사고는 클라이언트의 절반이 실패였을 뿐인데, 클라이언트들이 만든 사고는 전체가 실패가 됩니다.

제공자가 필요로 하는 건 시간입니다. 그리고 그 시간을 내 줄 수 있는 쪽은 백오프하는 클라이언트입니다. 즉 과부하 대응은 더 빨리 처리하는 것이 아닙니다. 일부 요청을 일부러 늦추거나 거절하는 것이 핵심입니다. 늦추고 거절하지 않으면, 지연은 재시도로, 재시도는 더 큰 지연으로, 다시 재시도로 이어집니다. 이 글의 나머지 부분은 이 순환을 세 개의 지점에서 끊는 법입니다.

핵심 개념 요약 인포그래픽 1 NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.

용량을 늘리는 것으로는 이 순환을 이기지 못한다

직감은 늘 서버를 더 넣는 쪽으로 갑니다. 그리고 대부분 옳습니다. 용량이 실제로 부족하다면 확장은 답입니다. 하지만 재시도 폭풍은 선형이 아니라 지수적입니다. 정상 부하의 두 배, 네 배, 여덟 배가 매 라운드 쌓인다면, 인스턴스를 늘리는 속도로는 그 곡선을 따라잡을 수 없습니다. 확장은 선형이고 폭풍은 지수적입니다. 두 직선이 만나는 지점에서는 늘 폭풍이 이깁니다.

LLM API를 부르는 서비스라면 문제가 한 번 더 커집니다. LLM의 용량은 CPU나 메모리가 아니라 돈입니다. 토큰당 과금이고 응답은 몇 초를 먹으며 길이까지 모델이 정합니다. 인스턴스를 두 배로 늘려도 토큰 단가는 한 토큰도 안 떨어집니다. 하루에 100개 요청만 처리하던 기능이, 사용자가 긴 문서를 통째로 붙여넣는 날에는 한 시간 안에 월 예산을 태울 수 있습니다. 이런 구조에서 더 많이 처리는 비용 폭주로 직결됩니다.

그래서 과부하 대응의 목표는 처리량을 최대화하는 게 아닙니다. 목표는 시스템이 무너지지 않는 최대 부하를 미리 정해 두고 그 한도를 넘기려는 요청을 정중하게 거절하는 것입니다. 무너지지 않으면 재시도가 발생하지 않고 두 번째 물결도 없습니다. 버티는 시스템은 천천히라도 혼자 회복하지만, 무너진 시스템은 자기가 부른 폭풍을 자기 손으로 잠재워야 합니다.

이 관점에서 레이트 리미팅은 기능이 아니라 안전장치입니다. 피크를 더 버티게 하는 것이 아닙니다. 피크가 시스템 전체를 끌고 가지 못하게 하는 것이 핵심입니다. 다음 세 섹션은 이 안전장치를 서버, 클라이언트, LLM API의 세 지점에 어떻게 세우는지를 다룹니다.

서버의 근육: 토큰 버킷과 429

서버가 지금은 되고 지금은 안 된다를 정하는 방식은 알고리즘만큼만 정확합니다. 가장 직관적인 것은 고정 윈도우입니다. 1분 단위로 요청을 세고 100개를 넘으면 429를 보냅니다. 매 분의 시작에서 카운터를 제로로 만듭니다. 한도는 숫자 하나, 카운터도 하나입니다. 문제는 경계에서 나옵니다.

성실한 클라이언트가 한 분의 마지막 1초에 100개를 보내면, 다음 분의 첫 1초에 또 100개를 보낼 수 있습니다. 2초 사이에 200개입니다. 분당 평균으로 보면 한도인데, 순간 피크로는 한도의 두 배가 통과합니다. 그 순간 피크를 데이터베이스가 버지지 못한다면, 경계 문제는 주말 밤 11시가 됩니다. 고정 윈도우의 가치는 단순함에 있고 저트래픽 서비스의 거친 한도에는 충분합니다.

경계 문제를 고치려면 지금부터 거꾸로 60초를 봐야 합니다. 모든 요청에 시각을 붙이고 지금 끝나는 윈도우 안에 몇 개가 있는지 세웁니다. 결과는 정확합니다. 2초 경계에서 200개는 어디에서도 발생할 수 없습니다. 그러나 대가가 있습니다. 최근 요청들의 시각을 윈도우에서 벗어날 때까지 보관해야 합니다. 트래픽이 많은 엔드포인트에서는 사용자당 수천 개의 엔트리가 쌓이고, 한도 장치가 스스로 자원을 먹습니다. 1인 개발자에게 슬라이딩 윈도우는 토큰 버킷으로는 부족해져서야 갈 때의 도구입니다.

토큰 버킷은 프로덕션 서비스 대부분이 쓰는 일꾼입니다. 숫자 두 개로 돌아갑니다. rate는 초당 버킷에 몇 개가 채워지는가로, 지속할 수 있는 속도를 정합니다. burst는 버킷의 크기로, 한 순간에 몇 개까지 허용하는지를 정합니다. 요청이 오면 토큰을 하나 빼고 토큰이 없으면 429를 보냅니다. rate가 평균 부하를, burst가 허용할 순간 피크를 정하기 때문에 고정 윈도우의 경계 문제도 없고 슬라이딩 윈도우의 저장 부담도 없습니다.

방식 경계 버스트 저장 비용 1인 서비스 적합도
고정 윈도우 2배 피크가 통과 카운터 하나 저트래픽이면 충분
슬라이딩 윈도우 없음 요청당 시각 보관 부족해져서야
토큰 버킷 burst로 제어 숫자 두 개 기본 선택

세 가지 한도를 나란히 두면 선택이 보입니다. 고정 윈도우는 단순하지만 경계에서 두 배가 새고 슬라이딩 윈도우는 정확하지만 요청마다 시각을 보관합니다. 토큰 버킷은 burst 값으로 순간 피크를 직접 정하는 데다 숫자 두 개로 돌아가기 때문에 기본 선택이 됩니다.

429를 보낼 때는 두 가지를 잊지 마십시오. Retry-After 헤더를 붙여서 다시 올 때까지 기다릴 시간을 클라이언트에게 말해 주는 것. 그리고 429가 서버가 죽어서가 아니라 한도에 닿아서 보내지는 것임을 코드와 로그에서 분명히 하는 것. 죽은 서버가 내뱉는 429는 타임아웃과 구별되지 않고, 클라이언트는 또다시 폭풍을 부릅니다. 살아 있는 서버가 의도적으로 보내는 429만, 클라이언트가 백오프를 시작할 근거가 됩니다.

클라이언트의 근육: 백오프와 지터

서버에서 429를 보내는 것은 이야기의 반쪽입니다. 나머지 반쪽은 그것을 받는 클라이언트입니다. 내 서비스가 결제 제공자, LLM, 데이터베이스를 부를 때, 나는 이미 누군가의 레이트 리미트 뒤에 서 있는 클라이언트입니다. 현장에서 가장 흔한 재시도 코드는, 예외가 나면 바로 다시 부르는 두 줄짜리입니다. 이 코드는 제공자가 흔들리기 시작하면 모든 요청을 거의 같은 순간에 실패시키고, 모든 클라이언트를 거의 같은 순간에 재시도하게 만듭니다.

재시도가 안전하려면 두 조건 중 하나가 깨져야 합니다. 재시도가 실패보다 충분히 늦거나, 다른 클라이언트들과의 시점이 어긋나거나. 지수 백오프는 전자를, 지터는 후자를 담당합니다. 지수 백오프는 재시도 간격을 1초, 2초, 4초, 8초처럼 늘려 제공자가 회복하는 시간을 줍니다. 지터는 그 간격에 무작위 요소를 섞어, 수천 개 클라이언트가 같은 순간에 재시도하지 못하도록 어긋나게 만듭니다.

같은 1,000개 클라이언트 예시를 다시 보겠습니다. 지수 백오프와 지터가 붙어 있으면, 처음에 500개가 실패해도 재시도는 한 순간에 몰리지 않고 넓게 퍼집니다. 제공자는 정상 요청과 재시도가 겹치지 않는 형태로 부하를 다시 받습니다. 부하는 두 배가 아니라, 회복 속도에 맞춰 서서히 줄어듭니다. 반대로 지터가 없는 동시 재시도는 매 라운드 두 배씩 부하를 키우고, 제공자가 분 단위까지 회복하지 못하는 이유가 됩니다.

혼잡할 때 버리는 법, 즉 로드 셰딩도 함께 갖춰야 합니다. 모든 요청을 재시도하지는 않습니다. 우선순위가 낮은 요청은 재시도 없이 바로 거절하고 핵심 요청만 백오프를 타게 합니다. LLM을 부르는 서비스라면 요약 같은 비핵심 호출은 과부하 시 먼저 흘려보냅니다. 버린 요청은 클라이언트에게 429로 돌아오고 백오프를 통해 나중에 다시 옵니다. 즉시 처리를 미루는 것보다, 전체를 무너뜨리지 않는 쪽이 항상 낫습니다.

LLM API: 예산은 모니터링이 아니라 제어다

LLM을 서비스에서 부르기 시작하면 위험의 모양이 바뀝니다. 보통 API 호출은 비용에 상한이 있습니다. 밀리초 몇의 계산, 정가 하나. LLM 호출은 토큰당 과금이고 응답은 몇 초를 먹고 길이까지 모델이 정합니다. 그래서 과부하 대응은 트래픽 하나를 막는 게 아닙니다. 트래픽과 비용을 동시에 막아야 합니다. 청구서와 내 서비스, 둘 다 지켜야 합니다.

가장 흔한 첫 번째 실수는 LLM 비용을 모니터링 문제로 푸는 것입니다. 지출이 올라가는 것을 보여주는 대시보드는 사후에 쓸모가 있습니다. 자고 있으면 쓸모가 없습니다. 대시보드가 어제 지출이 300달러였습니다라고 말하는 동안, 서비스는 오늘 밤에 예산을 태우고 있습니다. 보이는 것은 이미 지난 일이기 때문입니다.

동작하는 모델은, 예산이 집행되고 지출에 천장이 있다는 것입니다. 예산은 세 개입니다. 일일 예산은 하루에 잃어도 되는 절대 상한입니다. 요청당 예산은 한 호출이 태울 수 있는 최대값으로, 입력은 컨텍스트 길이로, 출력은 최대 토큰으로, API 파라미터로 씁니다. 사용자당 예산은 한 계정이 하루에 태울 수 있는 상한입니다. 나를 깨뜨리는 건 대개 큰 사용자 하나인데, 사용자당 천장이 그 한 마리를 상한이 있는 계좌로 바꿉니다. 세 개 모두 서버 쪽, 호출 직전에 집행합니다.

일일 예산이 다 쓰이면, 핵심이 아닌 LLM 기능은 정중한 문장으로 대체됩니다. AI 답변은 오늘 자리가 찼습니다, 내일 다시 시도해 주세요. 이 문장은 청구서보다 낫고 침묵보다 낫습니다. 재시도를 부르는 타임아웃이 아닙니다. 재시도가 필요 없는 명확한 거절입니다. LLM 예산 고갈도 결국 과부하의 한 형태이고 대응은 똑같습니다. 무너지지 않게, 의도적으로 거절합니다.

계산을 하나 해 두겠습니다. 단가는 예시로 씁니다. 입력 100만 토큰당 3달러, 출력 100만 토큰당 15달러. [예시 단가입니다. 내가 쓰는 모델의 가격표를 대입해야 합니다.] 질문답변 호출 하나를 입력 2,000 토큰, 출력 300 토큰으로 잡으면, 한 호출의 비용은 대략 0.0105달러입니다. [추정] 하루 10만 호출이면 약 1,050달러입니다. 이 숫자가 요청당 예산과 사용자당 예산을 정하는 기준이 됩니다. 호출당 상한을 0.1달러로 잡으면, 한 사용자가 10만 토큰을 붙여넣어도 그 이상의 청구서는 나가지 않습니다.

과부하 대응은 기능이 아니라 규율이다

세 가지 통제를 다시 보면 공통점이 보입니다. 서버의 레이트 리미트, 클라이언트의 백오프와 로드 셰딩, LLM API의 예산 집행. 전부 사고가 나기 전에 한도를 정해 두는 일입니다. 전부 시스템이 지금은 안 돼, 조금 후에 와라고 말할 수 있는 존재를 미리 만들어 놓는 일입니다. 이 말은 피크가 올 때 자동적으로 나오는 것이 아니라, 설계할 때 의도적으로 넣어야 하는 것입니다.

1인 개발자로서 프로덕션을 돌리며 배운 것은, 과부하에서 서비스를 살리는 것이 가장 어려운 일이고 가장 비싼 일이라는 것입니다. 무너진 뒤의 재시도 폭풍을 잠재우는 데 쓰는 에너지는, 미리 한도를 정해 두는 데 쓰는 에너지의 몇 배입니다. 그리고 LLM API를 부르는 서비스라면 그 대가는 트래픽이 아니라 청구서로 돌아옵니다.

그래서 결론입니다. 트래픽이 폭주하면 더 빨리 처리하려고 하지 마십시오. 의도적으로 늦추고 의도적으로 거절하십시오. 서버는 토큰 버킷으로 문 앞에서 수를 정하고 클라이언트는 지수 백오프와 지터로 물러서며 LLM 예산은 호출 직전에 천장으로 막으십시오. 그 세 가지만으로도, 시스템은 자기가 부른 부하에 죽지 않게 됩니다. 무너지지 않는 기술은 확장하는 기술이 아닙니다. 지금은 안 돼라고 말하는 기술입니다.

토큰 버킷의 구현 코드와 예산 집행을 넣는 위치, 로드 셰딩의 우선순위 표까지, 더 깊이 보고 싶다면 이 주제를 정리한 34페이지 전자책이 있습니다. PDF에서 구현 단서까지 챙기면, 이 글의 세 가지 통제를 내 서비스의 코드로 옮기는 작업은 한두 시간의 일입니다. [추정]

참고 자료

본문의 토큰 버킷, 429, 지수 백오프와 지터는 아래 자료와 대조해 보실 수 있습니다.

태그: backpressure, cascade-failure, llm-api-cost, load-shedding, rate-limiting, retry-storm, token-bucket

카테고리:

업데이트: