LLM 앱 운영 #1 데모와 프로덕션 사이 — 운영의 지도
LLM 앱 개발 실전에서 기능을 만들었고, AI 에이전트 개발 실전과 RAG 심화에서 품질을 끌어올렸습니다. 그런데 기능이 동작하는 것과 서비스를 운영할 수 있는 것은 다른 문제입니다. 사용자가 늘면 비용이 어디로 새는지, 응답이 왜 가끔 느린지, API가 한도를 돌려줄 때 무엇을 해야 하는지 같은 질문이 시작됩니다. 이 시리즈는 그 질문들, 즉 LLM 앱의 운영을 7편에 걸쳐 다룹니다.
운영의 다섯 축 #
LLM 앱 운영의 관심사는 다섯 가지로 정리됩니다. 이 시리즈의 지도이기도 합니다.
| 축 | 질문 | 다루는 편 |
|---|---|---|
| 비용 | 요청 하나에 얼마가 들고, 어디서 줄이는가 | 2편(회계·라우팅), 3편(캐싱), 4편(배칭) |
| 지연 | 사용자가 얼마나 기다리는가 | 3편(캐싱), 5편(타임아웃) |
| 신뢰성 | 한도와 장애에 어떻게 버티는가 | 5편 |
| 품질 | 답이 좋아지고 있는가, 나빠지고 있는가 | 7편(평가의 정기 운영) |
| 보안 | 누가 내 앱을 조종하려 드는가 | 6편 |
일반 백엔드 운영과 겹치는 듯하지만 무게중심이 다릅니다. 보통의 API 호출은 비용이 사실상 균일한데, LLM 호출은 요청마다 비용이 다르고 그 편차가 수백 배까지 납니다. 응답이 수십 초를 정상적으로 넘기는 것도, 입력에 섞인 텍스트가 앱의 동작을 바꿀 수 있다는 것도 LLM 특유의 사정입니다. 그래서 운영 도구도 LLM에 맞게 다시 챙겨야 합니다.
모든 것의 출발점 — 요청 단위 계측 #
다섯 축 어디를 개선하든 전제는 같습니다. 지금 얼마나 쓰고 있는지를 요청 단위로 아는 것입니다. 다행히 재료는 모든 응답에 들어 있습니다. usage 필드입니다.
import json, time, logging
logger = logging.getLogger("llm")
def call_llm(**kwargs):
start = time.monotonic()
response = client.messages.create(**kwargs)
latency = time.monotonic() - start
u = response.usage
logger.info(json.dumps({
"request_id": response._request_id, # API가 부여하는 요청 ID
"model": response.model,
"input_tokens": u.input_tokens,
"output_tokens": u.output_tokens,
"cache_read": u.cache_read_input_tokens, # 3편에서 본격적으로 쓴다
"cache_write": u.cache_creation_input_tokens,
"latency_s": round(latency, 2),
"stop_reason": response.stop_reason,
"feature": kwargs.get("metadata", {}).get("feature", "unknown"),
}))
return response호출을 이 래퍼 하나로 통일하는 것이 1편의 실습 전부입니다. 짚어 둘 포인트가 세 가지 있습니다.
- request_id를 남깁니다. 응답의
_request_id는 API 쪽 추적 식별자입니다. 장애나 이상 응답을 문의할 때 이 값이 있어야 양쪽 로그를 이을 수 있습니다. - 기능 태그를 붙입니다. “전체 비용이 늘었다"는 것만으로는 원인을 알 수 없지만, “요약 기능의 비용이 늘었다"는 것은 행동할 수 있는 정보입니다. 호출마다 어느 기능의 호출인지 태그를 남겨야 비용을 기능별로 가를 수 있습니다.
- stop_reason도 기록합니다.
max_tokens로 잘린 비율,refusal비율은 품질 문제의 조기 신호입니다.
토큰이 곧 돈입니다 — 단가의 구조 #
로그에 쌓인 토큰을 돈으로 바꾸는 환산표가 필요합니다. 기억할 구조는 두 가지입니다. 모델마다 단가가 다르고, 입력보다 출력이 비쌉니다.
| 모델 | 입력(1M 토큰) | 출력(1M 토큰) |
|---|---|---|
| claude-opus-4-8 | $5.00 | $25.00 |
| claude-sonnet-4-6 | $3.00 | $15.00 |
| claude-haiku-4-5 | $1.00 | $5.00 |
(시점에 따라 바뀌므로 공식 가격표를 기준으로 삼습니다.) 출력 단가가 입력의 5배라는 비대칭이 운영 감각의 기초입니다. 입력 1만 토큰에 출력 500토큰인 전형적인 RAG 요청이라면, 토큰 수로는 입력이 95%지만 비용에서는 출력이 20%를 넘게 차지합니다. 그리고 같은 요청을 haiku로 처리할 수 있다면 비용은 5분의 1이 됩니다. 이 두 지렛대(출력 길이, 모델 선택)를 2편에서 본격적으로 당깁니다.
PRICE = { # 1M 토큰당 달러 (입력, 출력)
"claude-opus-4-8": (5.00, 25.00),
"claude-sonnet-4-6": (3.00, 15.00),
"claude-haiku-4-5": (1.00, 5.00),
}
def cost_usd(model: str, input_tokens: int, output_tokens: int) -> float:
inp, out = PRICE[model]
return input_tokens / 1e6 * inp + output_tokens / 1e6 * out이 함수를 로그 파이프라인에 붙이면 “기능별 일일 비용” 그래프가 나옵니다. 운영 대시보드의 첫 번째 패널입니다.
기준선 — 운영의 비교 대상 만들기 #
RAG 심화 1편에서 품질 개선의 기준선을 만들었듯이, 운영에도 기준선이 필요합니다. 계측을 일주일만 돌리면 이런 숫자가 손에 잡힙니다.
- 기능별 요청당 평균·95분위 비용
- 요청당 평균·95분위 지연
- 일일 토큰 총량과 비용
- stop_reason 분포(잘림 비율, 거부 비율)
이후 시리즈의 모든 기법(캐싱, 배칭, 라우팅)은 이 기준선과의 비교로 효과를 판정합니다. “캐싱을 켰더니 좋아진 것 같다"가 아니라 “입력 비용이 41% 내려갔다"로 말할 수 있게 하는 토대입니다.
흔히 걸려 넘어지는 곳 #
- 월말 청구서로 비용을 처음 본다 — 청구서는 어디서 샜는지 말해 주지 않습니다. 요청 단위 usage 로깅이 있어야 원인 추적이 가능합니다.
- 호출 코드가 흩어져 있다 — 계측 없는 호출이 한 군데라도 남으면 그 기능은 시야 밖입니다. 래퍼 함수로 호출 경로를 하나로 모읍니다.
- 평균만 본다 — LLM 비용과 지연은 꼬리가 깁니다. 평균이 안정적이어도 95분위가 흔들리면 일부 사용자는 이미 나쁜 경험을 하고 있습니다.
마무리 #
이번 글에서는 운영의 지도를 그리고 계측의 토대를 만들었습니다.
- 운영의 관심사는 비용, 지연, 신뢰성, 품질, 보안 다섯 축입니다. LLM은 요청마다 비용이 다르다는 점이 일반 백엔드와의 가장 큰 차이입니다.
- 모든 호출을 래퍼로 모아 usage, 지연, stop_reason, 기능 태그를 요청 단위로 남깁니다.
- 단가의 구조(모델 차이, 출력이 입력의 5배)를 알고, 일주일의 계측으로 기준선을 만듭니다.
다음 글인 “LLM 앱 운영 #2 비용 — 토큰 회계와 모델 라우팅"에서는 이 계측 위에서 비용을 실제로 줄이기 시작하겠습니다. 가장 큰 지렛대인 모델 선택을 작업 난이도별로 체계화하는 방법입니다.