LLM 지연 시간을 줄이는 일은 반응성 높은 AI 애플리케이션을 만드는 엔지니어에게 가장 중요한 과제 중 하나입니다. 대규모 언어 모델(LLM)의 성능은 계속 발전하고 있지만, 토큰을 하나씩 생성하는 방식은 최종 사용자에게 답답한 병목을 만들 수 있으며, 긴 대기 시간은 곧바로 참여도 저하와 애플리케이션 이탈로 이어집니다. 따라서 추론 파이프라인의 속도를 최적화하는 것은 개발자의 핵심 요구사항입니다. 이 가이드는 프롬프트 캐싱을 구성하고, 응답 스트리밍을 구현하며, 에지 네트워크 라우팅을 설계하고, 서버리스 구성을 활용해 처리 지연을 줄이는 방법을 설명합니다.
[!TIP] 성능 지표 팁: API 지연을 측정할 때는 첫 토큰까지의 시간(TTFT)을 전체 생성 속도와 분리해서 봐야 합니다. TTFT가 낮으면 전체 출력 생성에 몇 초가 걸리더라도 텍스트가 즉시 렌더링되기 시작하므로 사용자에게는 애플리케이션이 즉각적으로 느껴집니다.
핵심 요점:
- 프롬프트 캐싱: 정적 접두 헤더를 재사용해 파싱 단계를 건너뛰고 TTFT를 80% 줄입니다.
- 응답 스트리밍: 서버 전송 이벤트(SSE)로 토큰을 밀어 넣어 사용자가 즉각적인 텍스트 생성을 보게 합니다.
- 에지 워커: 인증과 요청 라우팅을 사용자와 가까운 지역 에지 센터에서 실행합니다.
- 모델 라우팅: 간단한 사용자 요청은 경량 모델로 우회시켜 속도를 최적화합니다.
LLM API 지연 시간의 구성 요소
응답 시간을 줄이려면 먼저 전체 API 지연을 좌우하는 요소가 무엇인지 이해해야 합니다. 전체 지연 시간은 세 가지 개별 변수의 누적 합계입니다.
첫째, 네트워크 전송 시간은 요청이 클라이언트에서 서버로, 그리고 다시 모델 제공자의 API로 이동하는 데 걸리는 시간을 측정합니다. 이 때문에 전송 거리가 주요 병목이 됩니다.
둘째, 첫 토큰까지의 시간(TTFT)은 모델이 요청을 받은 시점부터 첫 출력 토큰을 생성하기까지의 시간을 나타내며, 바로 이 때문에 프롬프트 캐싱이 매우 중요합니다.
마지막으로, 토큰 생성 속도는 하드웨어가 이후 토큰을 출력하는 속도를 측정합니다. 생성 속도는 하드웨어 제약이 좌우하지만, 개발자는 전송 시간과 TTFT를 완전히 제어할 수 있으므로, 똑똑한 라우팅과 캐싱으로 LLM 지연 시간을 크게 줄일 수 있습니다.
LLM 통합 속도 높이기사전 준비 사항
아래 최적화를 연결하기 시작하기 전에 다음 사항이 준비되어 있는지 확인하십시오. 특별한 것은 없지만, 하나라도 건너뛰면 나중에 혼란스러운 실패를 일으키기 쉽습니다.
- 직접 제어하는 미들웨어 또는 에지 런타임. 예제에서는 Cloudflare Workers를 사용하지만, 요청을 프록시할 수 있는 서버리스 플랫폼이면 무엇이든 동작합니다. 브라우저와 모델 제공자 사이에 위치할 곳이 필요합니다.
- 스트리밍과 캐싱을 지원하는 제공자의 API 자격 증명. OpenAI와 Anthropic 모두 지원합니다. 키는 클라이언트 측 코드가 아니라 시크릿(Wrangler 시크릿 또는 환경 변수)으로 저장하십시오.
- Node.js 18 이상 —
wrangler dev로 Workers를 로컬에서 테스트하려는 경우. 전체적으로 사용되는 전역fetch및ReadableStreamAPI는 해당 런타임과 최신 브라우저에서 사용할 수 있습니다. - 기준 측정값. 각 최적화가 실제로 도움이 되었음을 증명할 수 있도록, 무엇이든 바꾸기 전에 현재의 첫 토큰까지의 시간과 총 응답 시간을 기록해 두십시오. 아래 벤치마킹 섹션에서 방법을 설명합니다.
- 서버 전송 이벤트(SSE)에 대한 이해. 스트리밍 응답은
data:줄의 연속으로 도착하며, 클라이언트에서 이를 파싱하게 됩니다.
프롬프트 캐싱 구현하기
프롬프트 캐싱은 큰 시스템 프롬프트를 중심으로 구축된 애플리케이션에서 TTFT를 최적화하는 가장 효과적인 방법입니다. 요청에 긴 정적 명령 블록(에이전트의 시스템 프롬프트나 RAG 참조 문서 등)이 포함되어 있으면, 모델 제공자는 매 실행마다 그 토큰들을 파싱하고 인코딩해야 합니다. Anthropic과 OpenAI 모두 프롬프트 캐싱을 지원하며, 이는 파싱된 토큰 상태를 메모리에 저장합니다. 이후 같은 접두를 공유하는 요청은 파싱 단계를 건너뛰어 TTFT를 최대 80%까지 줄입니다.
캐시 유지 시간은 제공자마다 다릅니다. Anthropic은 약 5분간 비활성 상태가 지속되는 동안 캐시를 유지하는 반면, OpenAI는 동적 감쇠 모델을 사용합니다. 따라서 정기적인 백그라운드 페치 핑을 예약하면 중요한 시스템 명령을 서버 메모리에 활성 상태로 유지할 수 있습니다.
메커니즘은 두 제공자 간에 약간 다르며, 요청 구조를 올바르게 잡는 것이 캐시가 실제로 작동하는지를 결정합니다. Anthropic에서는 cache_control을 사용해 캐시 중단점을 명시적으로 표시합니다. 중단점 이전의 모든 것이 저장되므로, 안정적이고 정적인 콘텐츠가 먼저 오고 요청마다 변하는 콘텐츠가 마지막에 와야 합니다:
1import Anthropic from "@anthropic-ai/sdk";
2
3const anthropic = new Anthropic();
4
5const response = await anthropic.messages.create({
6 model: "claude-opus-4-8",
7 max_tokens: 1024,
8 system: [
9 {
10 type: "text",
11 text: SYSTEM_INSTRUCTIONS // small, sent on every request
12 },
13 {
14 type: "text",
15 text: KNOWLEDGE_BASE, // large, static reference block
16 cache_control: { type: "ephemeral" }
17 }
18 ],
19 messages: [
20 { role: "user", content: userQuestion } // volatile — after the breakpoint
21 ]
22});
23
24// Confirm the cache is working
25console.log(response.usage.cache_read_input_tokens);
가장 흔한 실수는 캐시된 블록 앞에 타임스탬프, 요청 ID, 또는 요청마다 달라지는 문자열을 두는 것입니다. 캐싱은 접두 일치이기 때문에, 중단점 앞 어디든 한 바이트만 바뀌어도 그 뒤의 모든 것이 무효화되고 캐시는 조용히 한 번도 적중하지 않습니다. 응답에서 usage.cache_read_input_tokens를 읽어 작동 여부를 확인하십시오. 동일한 요청에서도 이 값이 0으로 유지된다면, 동적인 무언가가 접두에 끼어든 것입니다. 또한 캐시된 접두는 캐싱이 아예 작동하기 전에 최소 길이(모델에 따라 대략 1,024~4,096 토큰)를 넘어야 한다는 점도 유의하십시오.
OpenAI는 더 단순한 방식을 취합니다. 약 1,024 토큰을 넘는 프롬프트에 대해 캐싱이 자동으로 이루어지며, 설정할 cache_control 플래그가 없습니다. 그래도 같은 규율이 여전히 적용됩니다. 정적 명령 블록을 messages 배열의 맨 앞에 두고 변하는 사용자 입력을 맨 끝에 붙여, 재사용 가능한 접두가 요청 간에 바이트 단위로 동일하게 유지되도록 하십시오.
프롬프트 캐싱의 가격 및 매개변수 구조를 확인하려면 Anthropic 프롬프트 캐싱 가이드 를 참조하십시오.
에지 컴퓨트와 서버리스 라우팅
LLM 요청을 하나의 중앙 집중식 서버에서 처리하면 전 세계 사용자에게 막대한 네트워크 홉이 발생합니다. API 미들웨어를 서버리스 에지 네트워크(예: Cloudflare Workers)에 배포하면 그 경로가 극적으로 짧아집니다.
에지 워커는 클라이언트 요청을 받아 세션을 인증하고, 가장 가까운 모델 제공자 데이터센터로 라우팅합니다. 이 서버리스 구조는 토큰이 계산되는 즉시 사용자 화면에 전달하므로 인터페이스가 매우 반응적으로 느껴집니다. 아래 JavaScript 미들웨어는 에지 런타임에서 직접 스트리밍 응답을 구성하는 방법을 보여줍니다:
1export default {
2 async fetch(request, env) {
3 const payload = await request.json();
4
5 // Call the streaming LLM endpoint
6 const response = await fetch("https://api.openai.com/v1/chat/completions", {
7 method: "POST",
8 headers: {
9 "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
10 "Content-Type": "application/json"
11 },
12 body: JSON.stringify({
13 model: "gpt-4o-mini",
14 messages: payload.messages,
15 stream: true
16 })
17 });
18
19 // Forward the stream directly to the client browser
20 return new Response(response.body, {
21 headers: { "Content-Type": "text/event-stream" }
22 });
23 }
24};
이 서버리스 구조는 토큰이 계산되는 즉시 사용자 화면에 전달합니다. 에지에 최적화된 백엔드를 구축하는 방법을 배우려면 Cloudflare Workers로 서버리스 API 구축하기 에 관한 가이드를 읽어 보십시오.
클라이언트에서 스트림 파싱하기
에지에서 스트림을 전달하는 것은 작업의 절반에 불과합니다. 브라우저는 여전히 도착하는 청크를 읽고 각 토큰을 렌더링해야 하며, 그렇지 않으면 응답이 버퍼에 쌓였다가 한꺼번에 나타나 목적을 무색하게 만듭니다. 이것은 대부분의 튜토리얼이 건너뛰는 단계이며, 체감 성능이 실제로 결정되는 지점입니다.
응답 본문은 원시 바이트의 ReadableStream입니다. SSE 프레임은 data: 줄로 도착하지만, 하나의 네트워크 청크에 여러 프레임이 담기거나 하나의 프레임이 두 청크로 나뉠 수 있으므로, 각 청크가 완전한 메시지라고 가정하는 대신 부분적인 줄을 버퍼링해야 합니다:
1async function streamCompletion(messages, onToken) {
2 const response = await fetch("/api/chat", {
3 method: "POST",
4 headers: { "Content-Type": "application/json" },
5 body: JSON.stringify({ messages })
6 });
7
8 const reader = response.body.getReader();
9 const decoder = new TextDecoder();
10 let buffer = "";
11
12 while (true) {
13 const { value, done } = await reader.read();
14 if (done) break;
15
16 buffer += decoder.decode(value, { stream: true });
17 const lines = buffer.split("\n");
18 buffer = lines.pop(); // keep the trailing partial line
19
20 for (const line of lines) {
21 if (!line.startsWith("data: ")) continue;
22 const payload = line.slice(6).trim();
23 if (payload === "[DONE]") return;
24
25 try {
26 const json = JSON.parse(payload);
27 const token = json.choices?.[0]?.delta?.content;
28 if (token) onToken(token);
29 } catch {
30 // ignore keep-alive comments and malformed partial frames
31 }
32 }
33 }
34}
buffer.split("\n") 다음에 오는 lines.pop()이 중요한 부분입니다. 이는 다음 청크가 완성할 때까지 불완전한 줄을 유지합니다. JSON.parse를 try/catch로 감싸면 킵얼라이브 주석이나 절반만 수신된 프레임이 도착해도 루프가 계속 살아 있습니다. 그런 다음 onToken 콜백이 각 조각을 DOM에 추가하므로, 모델이 텍스트를 생성하는 즉시 화면에 나타납니다.
지연 시간 측정 및 벤치마킹
측정하지 않은 것은 최적화할 수 없습니다. 각 변경 전후로 첫 토큰까지의 시간과 총 생성 시간을 기록해, 개선을 올바른 원인에 귀속시킬 수 있게 하십시오. TTFT를 가장 빠르게 표본화하는 방법은 time_starttransfer를 첫 바이트가 클라이언트에 도달하는 시점의 근사값으로 사용하는 curl입니다:
1curl -w "TTFT: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
2 -X POST https://your-worker.example.com/chat \
3 -H "Content-Type: application/json" \
4 -d '{"messages":[{"role":"user","content":"Hello"}]}' \
5 -o /dev/null -s
애플리케이션 수준의 수치를 얻으려면 클라이언트 파서를 직접 계측하십시오. 요청이 떠날 때와 첫 토큰이 도착할 때 각각 시계를 찍으십시오:
1const start = performance.now();
2let firstTokenAt = null;
3
4await streamCompletion(messages, (token) => {
5 if (firstTokenAt === null) firstTokenAt = performance.now();
6 render(token);
7});
8
9console.log(`TTFT: ${Math.round(firstTokenAt - start)}ms`);
각 측정을 여러 번 실행하고 단일 표본이 아니라 중앙값을 취하십시오. 네트워크 변동과 콜드 스타트가 일회성 측정값을 왜곡할 수 있기 때문입니다. 아래 표는 정상적으로 동작하는 전 세계 요청에서 시간이 일반적으로 어디에 소모되는지에 대한 예시 범위를 보여줍니다. 이 수치를 고정된 값이 아니라 비교할 형태로 취급하십시오. 실제 수치는 지역, 모델, 프롬프트 크기에 따라 달라지기 때문입니다.
| 단계 | 일반적 기여도 | 제어 가능 여부 |
|---|---|---|
| 네트워크 전송 (클라이언트에서 에지까지) | 10–60 ms | 예 — 에지 라우팅이 단축 |
| 에지에서의 미들웨어 처리 | 1–15 ms | 예 — 워커를 가볍게 유지 |
| 첫 토큰까지의 시간 (콜드 프롬프트) | 400–1,200 ms | 부분적으로 — 캐싱이 크게 단축 |
| 첫 토큰까지의 시간 (캐시된 접두) | 100–400 ms | 예 — 프롬프트 캐싱으로 |
| 토큰당 생성 | 10–50 ms/token | 아니오 — 모델과 하드웨어가 결정 |
주목할 만한 두 행은 캐시된 TTFT와 콜드 TTFT 수치입니다. 그 격차가 대부분의 애플리케이션에서 얻을 수 있는 가장 큰 단일 이득이며, 그래서 프롬프트 캐싱이 최적화 목록의 맨 위에 자리합니다. 반면 생성 속도는 제공자에 의해 고정되어 있으므로, 더 단순한 요청을 더 작은 모델로 라우팅하는 것이 그곳에서 유일한 지렛대입니다.
단계별 최적화 워크플로
소프트웨어 애플리케이션 속도를 최적화하려면, 정적 시스템 명령을 동적 사용자 입력과 분리하는 것부터 시작하십시오. 이 구분을 통해 캐시 진입 지점을 깔끔하게 겨냥할 수 있습니다.
다음으로, API 페이로드 안에서 프롬프트 캐싱 플래그를 활성화해 모델 제공자가 텍스트 토큰을 메모리에 저장하도록 하십시오.
항상 표준 SSE 엔드포인트를 사용해 응답 스트리밍을 구성하십시오. 청크가 도착하는 대로 처리하는 가벼운 프론트엔드 파서를 작성하면 사용자 체감 성능이 개선됩니다. 그런 다음 모델 폴백 경로를 마련하십시오. 기본적인 고객 입력은 더 작은 모델로 라우팅하고, 더 큰 추론 모델은 고급 작업을 위해 남겨 두십시오. 마지막으로, 네트워크 홉을 프로파일링해 서버리스 워커가 전송 지연을 줄이는지 확인하십시오. 데이터베이스 최적화에 대한 자세한 내용은 WordPress와 맞춤형 웹 개발 비교 가이드를 확인하십시오.
흔한 함정과 문제 해결
팀이 스트리밍과 캐싱을 처음 배포할 때 반복적으로 나타나는 몇 가지 실패가 있습니다. 증상을 알아보면 수많은 추측의 시간을 아낄 수 있습니다.
| 증상 | 유력한 원인 | 해결 방법 |
|---|---|---|
cache_read_input_tokens가 0으로 유지됨 | 타임스탬프, UUID, 또는 세션 ID가 캐시 중단점 앞에 있어 접두가 매 요청마다 바뀜 | 모든 동적 콘텐츠를 정적 블록 뒤로 옮기고, JSON을 결정적으로 직렬화 |
| 토큰이 점진적이 아니라 한꺼번에 도착함 | 중간 프록시나 CDN이 응답을 버퍼링함 | Cache-Control: no-transform과 X-Accel-Buffering: no를 전송하고, text/event-stream 콘텐츠 타입이 설정되었는지 확인 |
| 스트림이 도중에 끊김 | 업스트림 본문이 끝나기 전에 워커가 반환했거나 max_tokens에 도달함 | 전체 텍스트를 기다리지 말고 response.body를 직접 반환하고, max_tokens를 높임 |
| 캐싱에도 불구하고 첫 토큰이 느림 | 정적 접두가 제공자의 최소 캐시 가능 길이 미만임 | 명령을 통합해 캐시된 블록이 약 1,024 토큰 하한을 넘도록 함 |
| 일부 청크에서 클라이언트 파서가 예외를 던짐 | 프레임이 두 네트워크 청크로 나뉨 | 위에서 보인 대로 부분적인 줄을 버퍼링하고 JSON.parse를 try/catch로 감쌈 |
더 미묘한 함정은 에지 자체에서의 버퍼링입니다. 워커 안에서 반환하기 전에 await response.text()를 하면, 스트리밍 응답을 조용히 다시 블로킹 응답으로 바꾸는 것입니다. 항상 스트림 본문을 그대로 통과시키십시오. 마찬가지로 워커 CPU 한도에 주의하십시오. 미들웨어에서 요청마다 무거운 작업을 하면 그만큼 TTFT에 직접 더해지므로, 인증과 라우팅 로직을 최소한으로 유지하고 비용이 많이 드는 작업은 미루십시오.
프로덕션 고려 사항
브라우저에서 데모 스트리밍을 띄우는 것은 간단하지만, 실제 트래픽에서 안정적으로 실행하려면 몇 가지 보호 장치가 더 필요합니다.
업스트림 호출에 합리적인 요청 타임아웃을 설정해, 정체된 제공자 연결이 워커를 무한정 열어 두지 못하게 하고, 이를 기본 연결이 타임아웃될 때 두 번째 제공자나 더 작은 모델로 폴백하는 재시도와 짝지으십시오. 프롬프트 캐싱은 캐시 쓰기에는 약간의 프리미엄을, 읽기에는 큰 할인을 부과하므로, 접두가 재사용될 때만 이득이 됩니다. 몇 분마다 백그라운드 핑을 보내면 매 사용자 요청마다 다시 쓰는 비용을 치르지 않고도 인기 있는 시스템 프롬프트를 상주 상태로 유지할 수 있습니다.
출시 시점에만이 아니라 지속적으로 계측하십시오. 요청마다 TTFT와 초당 토큰 수를 기록하고 중앙값이 표류할 때 경보를 울리십시오. 제공자 측 회귀나 프롬프트 크기 변화가 거기서 가장 먼저 드러나기 때문입니다. 마지막으로, 제공자의 속도 제한을 존중하십시오. 동시 스트림이 폭주하면 제한에 걸릴 수 있으므로, 요청이 조용히 실패하게 두는 대신 부하를 우아하게 큐잉하거나 흘려보내십시오. 이러한 조치가 빠른 프로토타입을 중요한 순간에도 빠르게 유지되는 애플리케이션으로 바꿔 줍니다.
핵심 요점
- 첫 토큰까지의 시간(TTFT)과 전송 시간을 겨냥해 LLM 지연 시간을 줄이십시오.
- 모델 API의 프롬프트 캐싱을 활용해 시스템 명령 파싱 오버헤드를 우회하십시오.
- 응답 스트리밍을 사용해 토큰을 실시간으로 전달하여 체감 속도를 개선하십시오.
- API 미들웨어를 서버리스 에지 런타임에 배포해 전 세계 네트워크 경로를 단축하십시오.
- 더 단순한 사용자 쿼리를 경량 모델로 라우팅해 실행 속도를 최적화하십시오.
- 성능 모니터링 도구를 설정해 실제 사용자 조건에서 지연 시간을 지속적으로 분석하고 줄이십시오.
자주 묻는 질문 (FAQ)
프로덕션에서 LLM 지연 시간을 어떻게 줄이나요? 프로덕션에서 LLM 지연 시간을 줄이려면, 정적 명령에 대한 프롬프트 캐싱을 구현하고, 토큰 스트리밍을 활성화하며, 에지 워커를 배포해 요청 라우팅을 최적화해야 합니다. 서버리스 오케스트레이션 런타임을 전 세계 클라이언트에 더 가깝게 배포하면, 개발자는 여러 네트워크 라우팅 홉을 우회하고 첫 응답 토큰을 실시간으로 전달합니다.
프롬프트 캐싱이란 무엇인가요? 프롬프트 캐싱은 파싱된 텍스트 상태를 서버 메모리에 저장하는 API 기능으로, 같은 접두를 사용하는 이후 요청이 훨씬 빠르게 실행되도록 합니다. 큰 명령 데이터셋에 대한 시스템 파싱 주기를 우회함으로써, 이 최적화는 첫 토큰까지의 시간(TTFT)을 최대 80%까지 줄입니다.
모델 크기가 지연 시간에 영향을 주나요? 네, 더 작은 모델은 토큰 생성 속도가 훨씬 빨라, 지연 시간이 주요 관심사인 단순한 작업에 이상적입니다. 간단한 분류나 추출 요청을 전문화된 에지 모델로 라우팅하면 빠른 처리 시간을 보장하면서, 밀도 높은 모델은 추론 작업을 위해 남겨 둘 수 있습니다.
서버 전송 이벤트(SSE) 스트리밍은 체감 지연 시간을 줄이는 데 어떻게 도움이 되나요? SSE 스트리밍은 모델 호스트에서 텍스트 출력 토큰이 컴파일되는 대로 실시간으로 클라이언트 화면에 밀어 넣습니다. 이것이 총 실행 시간을 줄이지는 않지만, 첫 토큰까지의 시간(TTFT)을 최소화하고 사용자에게 반응적이고 활발한 애플리케이션 인터페이스를 제공합니다.
동적 LLM 응답을 에지에서 어떻게 캐싱하나요? 짧은 TTL(Time to Live) 한도를 가진 KV 데이터베이스나 Redis 인스턴스를 사용해 에지에서 동적 응답을 캐싱할 수 있습니다. 동적 응답 캐싱은 반복적인 사용자 쿼리나 공통적인 고객 서비스 의도에 효과적이며, 모델 제공자 네트워크 호출을 완전히 방지합니다.
댓글