Skip to main content

23장. 로그, 트레이스, 실패 분석

LLM 서비스 운영에서 가장 무서운 장면은 에러 화면이 아닐 때가 많다. HTTP 200이 떨어졌고 서버도 멀쩡한데, 사용자는 엉뚱한 답을 받는다. 어떤 요청은 1초 만에 끝나고, 어떤 요청은 12초를 기다리게 만든다. 어떤 세션은 멀쩡한데, 특정 테넌트에서만 툴 호출이 꼬인다. 전통적인 웹 서비스라면 에러율과 응답 시간만 봐도 감이 왔겠지만, LLM 서비스는 그렇게 단순하지 않다. 실행은 성공했는데 결과는 실패일 수 있기 때문이다.1

그래서 이 장의 핵심은 단순하다. LLM 서비스는 안 보이면 고칠 수 없고, 추적되지 않으면 운영할 수 없다. 입력, 출력, 토큰, 지연 시간, 툴 호출, 폴백, 재시도, 세션 문맥을 한 요청 단위로 엮어 볼 수 있어야 한다. 그래야 "왜 틀렸는가", "왜 느렸는가", "왜 비싸졌는가"를 재현하고 개선할 수 있다.

5부. 서비스 품질과 운영 / 23장. 로그, 트레이스, 실패 분석

로그만으로는 부족하다

운영 초기에 흔히 하는 실수는 console.log()를 잔뜩 찍어 두고 안심하는 것이다. 물론 로그는 필요하다. 하지만 로그만으로는 인과관계를 놓치기 쉽다. 사용자의 질문이 들어와서 프롬프트가 만들어지고, 검색 결과가 붙고, 모델이 응답하고, 툴이 두 번 호출되고, 마지막에 후처리에서 JSON 파싱이 실패했다면, 이 흐름을 한눈에 보지 못하면 원인 찾기는 거의 추리 소설이 된다.

관측성은 더 넓은 개념이고, 트레이싱은 그 안의 핵심 기술이다. 관측성은 시스템의 내부 상태를 바깥으로 드러난 신호로 이해하는 능력이고, 트레이싱은 한 요청이 시스템 안을 어떻게 통과했는지 인과관계까지 보존하며 기록하는 방식이다. LLM 애플리케이션에서는 특히 트레이싱이 중요하다. 프롬프트, 응답, 툴 호출, 검색 단계, 커스텀 로직이 모두 하나의 요청 안에서 이어지기 때문이다.2

쉽게 비유하면 이렇다. 메트릭은 건강검진 결과표이고, 로그는 의사 메모이며, 트레이스는 수술 영상이다. 숫자만 보면 이상이 있다는 건 알 수 있다. 메모만 보면 사건은 알 수 있다. 하지만 실제로 어디서 무슨 일이 벌어졌는지 보려면 흐름이 보여야 한다.

LLM 관측성의 네 가지 신호

실무에서는 보통 네 가지 신호를 함께 본다.

  • 메트릭은 요청 수, 에러율, p95 지연 시간, 토큰 사용량처럼 추세를 본다.

  • 로그는 예외 메시지, 파싱 실패, 폴백 발생처럼 사건의 맥락을 남긴다.

  • 트레이스는 한 요청의 전체 흐름을 스팬 단위로 보여 준다.

  • 평가 신호는 답변 품질, 근거성, 안전성처럼 "잘 동작했는가"를 본다.13

이 네 가지가 함께 있어야 한다. 메트릭이 "어디가 아프다"를 알려 주고, 트레이스가 "왜 아픈지"를 보여 주며, 로그가 "그때 무슨 말을 했는지"를 보충하고, 평가 신호가 "사용자 입장에서 진짜 실패였는지"를 판단하게 해 준다.

이 장의 중심은 트레이스와 로그이지만, 메트릭과 평가 신호를 함께 엮지 않으면 반쪽짜리 운영이 된다. 예를 들어 p95 지연 시간은 괜찮은데 답변 품질이 급락했다면, 이는 성능 문제보다 프롬프트 버전이나 검색 문서 품질 문제일 가능성이 크다.

한 요청을 어떻게 추적할 것인가

트레이스는 "한 번의 사용자 요청"을 기준으로 잡는 것이 좋다. 그리고 그 안에 여러 개의 스팬을 둔다. 스팬은 실제 작업 단위다. 예를 들면 prompt.build, retrieval.search, llm.generate, tool.crm.lookup, response.postprocess 같은 식이다. 멀티턴 챗봇이라면 여러 트레이스를 하나의 세션으로 묶어야 한다. 그래야 이전 질문과 현재 실패를 연결해서 볼 수 있다.2

좋은 트레이스는 다음과 같은 구조를 가진다.

trace: req_01HV...
└─ span: chat.request
   ├─ span: prompt.build
   ├─ span: retrieval.search
   │  └─ span: vectordb.query
   ├─ span: llm.generate
   ├─ span: tool.calendar.create_event
   ├─ span: llm.finalize
   └─ span: response.validate

이 구조가 중요한 이유는 아주 실용적이다. 사용자가 "답이 이상하다"고 신고했을 때, 우리는 이렇게 질문할 수 있어야 한다.

"검색이 잘못됐나?" "프롬프트 조합이 잘못됐나?" "모델 응답은 맞았는데 툴 실행이 실패했나?" "실패 후 폴백이 동작했나?"

Datadog 같은 도구도 LLM 요청을 개별 추론 호출, 미리 정의된 워크플로, 혹은 에이전트 실행 전체를 하나의 트레이스로 보고, 그 안에 각 선택과 단계를 스팬으로 표현한다.5 중요한 건 특정 도구가 아니라 이 모델 자체다. 루트 스팬 하나, 단계별 자식 스팬 여러 개, 그리고 공통 식별자 하나. 이것이 운영의 기본 골격이다.

반드시 남겨야 하는 정보

트레이스를 만들었다고 끝이 아니다. 무엇을 남길지 정해야 한다. 빠뜨리기 쉬운 항목까지 포함해, 최소한 다음 정보는 있어야 한다.

  • 입력 정보: 사용자 입력, 시스템 프롬프트, 첨부 문서 요약, 선택된 컨텍스트. 원문 전체를 항상 저장하기 어렵다면 마스킹이나 샘플링을 적용한다.

  • 실행 정보: 모델 제공자, 모델명, 버전, temperature, max tokens, tool choice, prompt template 버전.

  • 출력 정보: 최종 응답, 구조화 출력 결과, finish reason, refusal 여부, 파싱 성공/실패.

  • 비용 정보: 입력 토큰, 출력 토큰, 총 토큰, 요청당 추정 비용, 기능별 비용.

  • 성능 정보: 전체 지연 시간, 첫 토큰까지의 시간, 토큰 스트리밍 속도, 큐 대기 시간.

  • 외부 상호작용: 검색 질의, 검색 결과 수, 선택된 문서 ID, 툴 이름, 툴 인자, 툴 응답 요약.

  • 운영 맥락: trace_id, session_id, user_id 또는 내부 사용자 키, tenant_id, environment, release version.

  • 오류와 복구: 예외 메시지, HTTP 상태, rate limit, timeout, retry 횟수, fallback 모델 사용 여부.6

여기서 핵심은 "나중에 물어볼 질문"을 미리 떠올리는 것이다. 운영자는 결국 이런 질문을 한다.

"어느 프롬프트 버전부터 품질이 떨어졌지?" "특정 모델에서만 비용이 급증했나?" "A 테넌트에서만 툴 타임아웃이 많나?" "폴백이 너무 자주 일어나서 체감 품질이 나빠졌나?"

이 질문에 답하려면, 기록은 처음부터 구조화되어 있어야 한다.

지연 시간은 한 덩어리로 보지 말고 분해하라

LLM 서비스의 지연 시간은 단순한 "응답 시간" 하나로 보면 중요한 문제를 놓친다. 특히 스트리밍 UI에서는 사용자가 느끼는 속도가 완전히 다르다. 여기서 자주 쓰는 세 지표가 있다.

TTFT(Time to First Token)는 요청을 보낸 뒤 첫 번째 토큰이 보일 때까지 걸린 시간이다. 사용자가 빈 화면을 바라보는 시간이라서 체감에 가장 민감하다. ITL(Inter-token Latency)은 토큰과 토큰 사이의 평균 지연이다. 첫 토큰은 빨리 나왔는데 이후가 끊기면 "버벅인다"는 느낌을 준다. E2E latency는 요청 시작부터 마지막 토큰까지의 전체 시간이다.4

이 셋을 나눠 봐야 하는 이유는 병목 지점이 다르기 때문이다. TTFT가 느리면 긴 프롬프트, 큐 적체, 모델 prefill, 네트워크 왕복이 의심된다. ITL이 느리면 모델 디코딩, 스트리밍 처리, 배치 경쟁이 문제일 수 있다. E2E만 높으면 출력 길이가 과도한 것일 수도 있다.

실무에서는 평균보다 백분위가 더 중요하다. p50은 평범한 날씨고, p95와 p99는 폭우다. 사용자는 대체로 폭우를 기억한다. 특히 유료 고객이나 대형 테넌트가 p99 구간에 몰리면, 서비스는 숫자상으로 멀쩡해도 실제로는 불만이 쌓인다.

로그는 사람이 읽고, 시스템이 묻게 만들어라

좋은 로그는 "예쁘게 찍힌 문자열"이 아니라 질의 가능한 구조화 데이터다. 예를 들어 다음과 같이 남겨야 한다.

{
  "timestamp": "2026-04-23T10:12:33.120Z",
  "level": "error",
  "trace_id": "req_01HV...",
  "session_id": "sess_92...",
  "tenant_id": "team_acme",
  "feature": "meeting_summary",
  "model": "gpt-4.1-mini",
  "prompt_version": "summary.v17",
  "event": "response.parse_failed",
  "error_code": "JSON_SCHEMA_MISMATCH",
  "retry_count": 1
}

이런 로그는 세 가지 장점이 있다. 첫째, 사람이 바로 읽을 수 있다. 둘째, 대시보드와 검색에서 필터링이 된다. 셋째, 트레이스와 메트릭에 붙이기 쉽다. 예를 들어 trace_id가 있으면 장애 상황에서 로그와 트레이스를 왕복하지 않고 한 흐름으로 본다.

로그 설계에서 중요한 원칙은 두 가지다. 텍스트 본문과 메타데이터를 분리할 것, 그리고 무한히 늘어나는 값을 인덱스 키로 쓰지 말 것이다. 사용자 원문, 프롬프트 전체, 응답 전문, conversation ID 같은 값은 검색 필드나 저장소 본문으로 두고, 메트릭 라벨이나 고카디널리티 태그로 남기지 않는 편이 안전하다. LLM 관측성에서는 프롬프트 텍스트나 user_id 같은 무한히 늘어나는 값을 라벨로 쓰면 저장 비용과 검색 성능이 빠르게 무너진다.3

대시보드는 무엇을 보여줘야 하는가

대시보드는 보기 좋은 벽지가 아니라, 운영자의 질문에 답하는 계기판이어야 한다. 최소한 다음 축은 따로 봐야 한다.

  • 트래픽과 안정성: 요청 수, 성공률, 에러율, timeout 비율, rate limit 비율

  • 속도: TTFT p50/p95/p99, E2E p50/p95/p99, 툴별 지연 시간, 검색 단계 지연 시간

  • 사용량과 비용: 입력 토큰, 출력 토큰, 요청당 평균 토큰, 기능별 비용, 테넌트별 비용

  • 행동 흐름: 툴 호출 수, 폴백 비율, 재시도 횟수, 세션 길이, 중단율

  • 품질: 포맷 준수율, 구조화 출력 파싱 성공률, 사용자 재시도율, 간단한 온라인 평가 점수37

여기서 특히 중요한 관점은 기능 단위 분리다. 전체 평균 비용은 대개 쓸모가 없다. "챗봇 전체 평균"이 아니라 support_chat, doc_summary, report_writer처럼 기능별로 나눠야 한다. 그래야 어느 기능이 토큰을 태우는지 보인다.

또 하나 중요한 것은 모델 단위 비교다. 같은 기능이라도 OpenAI, Claude, Gemini 또는 자체 모델 경로마다 지연 시간과 비용 특성이 다르다. 멀티 벤더 구조라면 공급자별 대시보드가 필수다. 그래야 폴백 정책이 진짜 효과가 있는지 검증할 수 있다.

실패 분석은 "틀렸다"에서 끝나면 안 된다

좋은 실패 분석은 결과에서 원인까지 내려간다. LLM 서비스에서는 보통 네 가지 유형으로 나눠 생각하면 실무적이다.

느려졌다

사용자가 "오늘따라 답이 늦다"고 말하면, 먼저 TTFT와 E2E 중 어디가 늘었는지 본다. TTFT가 튀었다면 프롬프트가 길어졌거나, 큐가 밀렸거나, 상류 서비스가 느린 것이다. E2E만 늘었다면 출력이 길어졌거나, 중간 툴 호출이 추가됐을 수 있다. 트레이스에서 가장 긴 자식 스팬을 보면 절반은 해결된다.

이때 평균만 보면 속는다. p95가 튀는지, 특정 테넌트만 그런지, 특정 모델만 그런지, 특정 프롬프트 버전 이후인지 반드시 잘라 봐야 한다.

비싸졌다

비용이 갑자기 늘면 우선 요청 수보다 토큰 수를 본다. LLM 비용은 대체로 요청 수보다 토큰 수에 더 민감하다. 긴 시스템 프롬프트가 배포됐는지, 검색 문서 개수가 늘었는지, 같은 요청에서 모델 호출 횟수가 증가했는지, 무한 루프에 가까운 툴 재호출이 있었는지 확인한다. 에이전트형 기능은 특히 이 문제가 자주 생긴다. 겉보기에는 성공 응답이지만, 내부에서는 불필요한 호출이 여러 번 돌았을 수 있다.

틀렸다

가장 어려운 유형이다. 이때는 최종 답변만 보지 말고, 그 답이 만들어진 중간 과정을 본다. 검색 결과가 부정확했는지, 프롬프트 조합에서 제약 조건이 빠졌는지, 툴이 잘못된 데이터를 돌려줬는지, 후처리 과정에서 일부 정보가 사라졌는지 확인한다. 사용자는 "AI가 틀렸다"고 말하지만, 실제 원인은 검색 인덱스, 툴 응답 스키마 변경, 프롬프트 버전 회귀일 수 있다.16

가끔만 망가진다

운영자를 가장 괴롭히는 유형이다. 이런 문제는 공통점 찾기가 핵심이다. 특정 입력 길이 이상에서만 실패하는지, 특정 모바일 네트워크에서만 TTFT가 느린지, 특정 테넌트의 데이터 형식에서만 파싱이 깨지는지 본다. 그래서 로그와 트레이스에는 길이, 포맷, 버전, 환경 같은 메타데이터가 꼭 있어야 한다.

개인정보와 비용을 지키는 관측성

관측성을 잘 만들면 디버깅은 쉬워지지만, 잘못 만들면 보안과 비용이 터진다. 특히 프롬프트와 응답에는 개인정보, 내부 문서, 고객 데이터가 그대로 들어오기 쉽다. 따라서 기본 원칙을 세워야 한다.

첫째, 원문 저장은 기본 허용이 아니라 기본 통제로 생각하자. 개발 환경에서는 비교적 자세히 남기더라도, 운영 환경에서는 마스킹, 샘플링, 부분 저장을 기본값으로 두는 편이 안전하다.36

둘째, 민감 정보는 span attribute나 metric label로 남기지 말자. attribute는 검색이 쉽지만, 그만큼 유출 범위도 넓어진다. 주민등록번호, 이메일, 전화번호, 카드 정보는 저장 전에 비식별화해야 한다.

셋째, 카디널리티를 통제하자. 프롬프트 전문, 응답 전문, request_id, conversation_id, raw tool arguments를 메트릭 라벨로 넣는 순간, 모니터링 시스템이 먼저 죽는다.3

넷째, 보존 기간을 다르게 가져가자. 디버깅용 원문은 짧게, 요약 메타데이터는 길게 가져가는 식으로 계층화하면 비용과 리스크를 함께 줄일 수 있다.

관측성은 많이 모으는 기술이 아니라, 필요한 것을 안전하게 모으는 기술이다.

알림은 장애를 알려야지, 공포를 퍼뜨리면 안 된다

모든 수치에 알림을 붙이면 운영자는 금방 무감각해진다. 알림은 "지금 사람이 개입해야 하는가"를 기준으로 설계해야 한다. 실전에서는 보통 다음 세 층이 유용하다.

  • 시스템 알림: 에러율 급증, timeout 증가, rate limit 폭증, 특정 툴 장애

  • 사용자 경험 알림: TTFT p95 초과, 구조화 출력 파싱 성공률 급락, 응답 중단율 상승

  • 비즈니스 알림: 기능별 비용 급증, 특정 테넌트의 토큰 사용량 폭증, 품질 점수 하락7

여기서 중요한 것은 임계값보다 맥락이다. 예를 들어 p95 지연 시간이 20% 증가해도 트래픽 급증 시간대의 일시적 현상일 수 있다. 반대로 에러율은 낮아도 고가치 고객 한 곳에서만 반복되면 즉시 대응해야 한다. 그래서 알림에는 trace 링크, 관련 대시보드 링크, 최근 배포 버전, 영향받는 기능 이름이 함께 붙는 것이 좋다.

좋은 알림은 "문제가 있다"에서 끝나지 않는다. "어디서 시작됐고, 누가 보고, 무엇부터 보면 되는지"까지 알려 준다.

최소한의 관측성부터 시작하는 도입 순서

처음부터 거대한 플랫폼을 만들 필요는 없다. 대신 순서를 지켜야 한다.

  1. 공통 식별자부터 통일한다. trace_id, session_id, tenant_id, feature, prompt_version이 먼저다.

  2. 루트 스팬과 핵심 자식 스팬을 심는다. LLM 호출, 검색, 툴 호출, 후처리 네 군데만 먼저 잡아도 훨씬 낫다.

  3. 구조화 로그를 붙인다. 문자열 로그 대신 JSON 로그로 바꾼다.

  4. 핵심 메트릭을 올린다. TTFT, E2E, 토큰 수, 파싱 성공률, fallback 비율 정도면 시작하기 충분하다.

  5. 실패 사례 리뷰를 정례화한다. 주간 운영 회의에서 "이번 주 최악의 트레이스 3개"를 함께 보는 습관이 정말 강력하다.

이 순서를 따르면 "도구를 도입했는데 아무도 안 본다"는 사태를 피할 수 있다. 관측성은 결국 문화이기도 하다. 로그를 남기는 습관, 원인을 추적하는 습관, 회귀를 숫자로 확인하는 습관이 함께 만들어져야 한다.

운영자가 실제로 묻게 될 질문들

챕터를 마무리하기 전에, 좋은 관측성 체계가 답해야 하는 질문을 정리해 보자.

  • 이 요청은 왜 9초나 걸렸는가?

  • 느린 원인은 모델인가, 검색인가, 외부 API인가?

  • 같은 질문인데 왜 어떤 사용자는 정답을 받고 어떤 사용자는 틀린 답을 받는가?

  • 어떤 프롬프트 버전부터 JSON 파싱 실패가 늘었는가?

  • 특정 고객의 비용이 왜 이번 주에 두 배가 되었는가?

  • 툴 호출 루프나 불필요한 재시도가 어디에서 발생하는가?

  • 안전장치나 폴백은 실제로 효과가 있었는가?

이 질문들에 5분 안에 답할 수 있다면, 서비스는 운영 가능한 상태에 가깝다. 반대로 한 질문에 반나절씩 로그를 뒤져야 한다면, 아직은 보이지 않는 상태다.

마치며

LLM 서비스 운영은 정답 맞히기보다 원인 찾기에 더 가깝다. 모델은 비결정적이고, 파이프라인은 여러 단계로 갈라지며, 실패는 조용히 숨어든다. 그래서 관측성은 부가 기능이 아니라 운영의 전제 조건이다.

입력과 출력을 남기고, 토큰과 비용을 계산하고, TTFT와 E2E를 분리해 보고, 툴 호출을 스팬으로 엮고, 에러를 구조화 로그로 남겨라. 그러면 장애는 덜 무섭다. 적어도 "무슨 일이 일어났는지"는 알 수 있기 때문이다.

운영에서 가장 비싼 것은 모델 요금이 아니다. 문제를 보지 못한 채 오래 헤매는 시간이다. 로그와 트레이스는 그 시간을 줄여 준다. 그리고 그 차이가, 실험용 데모와 운영 가능한 서비스의 경계를 만든다.

참고

1What is LLM observability? (Tracing, evals, and monitoring explained)

2LLM Observability & Application Tracing (Open Source) - Langfuse

3Observability for LLM Systems: Metrics, Traces, Logs, and Testing in Production

4Metrics — NVIDIA NIM LLMs Benchmarking

5LLM Observability

6From Black Box to Clear Picture: How to Trace LLM Agents and Find Failures

7Top Metrics for LLM Failure Alerts

책 목록

실전 LLM 서비스 개발: ChatGPT·Claude·Gemini API로 만드는 AI 웹 서비스의 설계부터 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기