LLM 서빙, 띄우는 것과 잘 띄우는 것 사이

김민규 · 토스증권 Machine Learning Engineer
2026년 8월 21일

LLM 서빙 생태계는 빠르게 바뀌고 있습니다. 불과 수년 전만 해도, LLM을 배포하는 것 자체가 허들인 경우가 많았어요. 하지만, 지금은 다양한 곳에서 on-premise LLM을 다양하게 사용하고 있습니다. 토스증권도 마찬가지로 다수의 오픈 소스 LLM, 직접 튜닝한 모델을 서비스에 활용하고 있습니다. 이런 상황에서, LLM을 단순히 서빙하는 것보다, 잘 띄우는 것이 더 중요해졌습니다.

평소에는 조용하던 팀의 알림 채널이 어느 날 갑자기 알림이 쏟아지는 날이 있습니다. 이 알림들은 여러 에러가 함께 묶여서 온 거예요. 100건 가까운 에러고, 대부분 타임아웃이라는 것까지는 알 수 있지만 이 알림만으로는 왜 이런 일이 생겼는지 알 수 없어요.

“어떤 지표를 봐야 이 알림의 원인을 알 수 있을까?”

2년 전, LLM 서빙을 처음 시작했을 때 풀어야 했던 문제는 두 가지였습니다.

1. 지표가 달라졌어요

기존 머신러닝 모델은 입력을 받아 한 건의 출력을 내놓는 구조였어요. 그래서 단 건의 응답 시간, 초당 몇 건의 트래픽을 처리하는지가 핵심 모니터링 지표였습니다.

그런데 LLM은 토큰을 순차적으로 생성해요. 단 건의 응답 시간이나 처리량은 변동성이 심하고, 애초에 통제할 수 있는 영역이 아닙니다. 그래서 지표를 이렇게 바꿨어요.

  • 처리량: 초당 처리 건수 → 초당 생성 토큰 수
  • 응답 시간: 최종 결과물까지의 시간 → 첫 번째 토큰이 나오기까지의 시간(TTFT)

2. 서빙 자체가 어려웠어요

토스증권은 쿠버네티스, Argo CD 같은 여러 기술 스택 위에서 움직여요. 그 위에 각자의 플랫폼을 얹어 모델을 서빙하고, 거기서 나온 로그와 메트릭을 자체 대시보드로 관찰하곤 했습니다.

문제는 이게 전부 서비스 개발자 개인의 역량에 의존했다는 점이에요.

그래서 ML 플랫폼을 직접 구축했습니다. 어려운 부분을 줄이고, 로그와 메트릭을 정규화해서 하나의 패널에서 볼 수 있게요. 서버 모니터링은 두 갈래로 나눴어요.

  • Grafana — 메트릭을 수집해 추이와 이상을 탐지
  • Kibana — 단건 로그를 확인

여기까지 만들고 나서, 대부분의 문제를 미리 예상하고 쉽게 해결할 수 있을 거라고 생각했습니다.

그런데 예상하지 못한 문제들이 생겼어요

앞서 구축한 모니터링만으로는 그 문제가 ‘왜’ 생겼는지 알 수 없었습니다. 원인을 파악하려면 어떤 지표를 봐야 할까? LLM 친화적인 지표를 이미 만들었는데도 부족했으니, 그 이상은 뭐가 있을까?

조사해보니 vLLM이나 SGLang 같은 서빙 프레임워크가 제공하는 지표들이 있었어요. 이걸 하나의 대시보드에 통합했습니다. 예전엔 헬스 상태, 오류 건수, 레이턴시 같은 일반적인 서버 대시보드였다면, 여기에 LLM에 특화된 지표를 얹은 거예요.

이 시스템을 갖추고 나서야 비로소 원인을 찾을 수 있게 된 사건이 세 가지 있습니다.

사건 1. 비어 있는 캐시

서비스 팀의 머신러닝 엔지니어분이 자신이 쓰는 모델을 서비스로 내보내려는데, 확인해보니 종종 타임아웃이 발생한다고 요청을 주셨어요. 그래서 먼저 알림이 온 시간과 트래픽이 몰리는 시간을 비교해봤어요.

그런데 둘이 일치하지 않았습니다. 원인이 트래픽이 아니라는 뜻이었죠.

지표들을 쭉 살펴보다가 시선을 잡아끄는 게 하나 있었어요. Prefix Cache Hit Rate였습니다. 옆의 지표들은 값이 나오는데, 이것만 값이 없었어요.

Prefix Cache가 뭐냐면

LLM은 토큰을 순차적으로 생성하고, 트랜스포머 구조에서 이전 문자열들에 대한 Key와 Value를 계속 참조합니다. 이걸 매번 계산해야 하는데 값은 똑같아요. 그래서 캐시 메모리에 저장해 두 번 계산하지 않게 하는 게 KV 캐시입니다.

KV 캐시는 하나의 시퀀스 안에서만 작동해요. 그런데 실제로 쓰다 보면 시스템 프롬프트처럼 앞부분을 공유하는 요청이 아주 많습니다. 서로 다른 요청이라도 앞쪽 KV 캐시는 동일한 경우가 많죠. 그걸 재사용하려고 쓰는 게 Prefix Cache고, Hit Rate는 그게 얼마나 재사용됐는지를 뜻해요. 높을수록 계산을 덜 하니 빨라집니다.

그런데 이 값이 0이었어요.

원인은 두 가지 중 하나였는데

지표 수집이 잘못됐거나, 아니면 실제로 요청들이 전부 달라서 히트가 안 났거나.

첫 번째는 아니었어요. 다른 모델에서는 정상적으로 값이 나왔거든요. 두 번째를 확인하려고 로그를 봤는데, 요청들이 많이 다르긴 했지만 그래도 어느 정도는 히트가 나야 하는 상황이었습니다.

이상해서 탐색을 시작했고, 재미있는 걸 발견했어요. 세팅을 잘못한 게 아니라, 사용 중이던 모델의 특성상 Prefix Caching이 기본적으로 꺼지는 설정이었습니다. 다른 모델들은 기본으로 켜져 있는데 이 모델만요.

다시 활성화해서 재배포했어요. 결과는 이랬습니다.

  • TTFT: 20초 가까이 걸리던 경우가 있었는데, 적용 후 약 1/10 수준으로 감소
  • Prefix Cache Hit Rate: 90% 이상 기록

사실 단순한 문제예요. 하지만 Prefix Cache Hit Rate라는 지표를 보고 있지 않았다면 발견하는 것 자체가 훨씬 어려웠을 겁니다.

사건 2. 완벽한 알리바이

또 알림이 쭈르륵 올라왔어요. 앞선 교훈대로 vLLM·SGLang이 제공하는 LLM 지표들을 확인했습니다.

그런데 이상하게도 특이 상황이 없었어요. TTFT가 조금 바뀌긴 했는데 차이가 20ms 수준이었고, 그마저도 P99였습니다. 메트릭상으로는 완벽한 알리바이였죠.

메트릭 문제가 아니라고 판단하고 로그로 시선을 옮겼습니다. 취합해보니 이상한 점이 하나 있었어요. finish_reasonlength 였습니다.

length는 저희가 지정한 상한(예: 최대 N토큰)을 넘어서 그냥 끊겼다는 뜻이에요. 즉 모델이 원하지 않는 말을 불필요하게 뱉어냈다는 의미죠.

요즘 많이 쓰는 리즈닝(thinking) 모델에서 이런 일이 꽤 발생합니다. 리즈닝을 훨씬 길게 하는 경향이 있으니까요. 그런데 신기한 건, 저희는 이걸 막으려고 리즈닝 토큰 개수를 제한하는 옵션을 이미 켜뒀다는 점이었어요. 그런데도 그게 실패하고 계속 생성됐습니다.

문제가 된 로그들의 출력을 하나하나 열어봤어요. 그러다 눈길을 끄는 문자열을 발견했습니다. 내용 자체는 중요하지 않고, 끝에 붙은 </think> 가 중요했어요.

이건 리즈닝이 끝났음을 알리는 스페셜 토큰입니다. 그런데 이게 리즈닝 구간이 아니라, 리즈닝이 끝나고 실제 출력을 뱉어야 하는 content 시점에 등장하고 있었어요.

여기까지 오니 더 손쓸 방법이 없어서 다시 탐색했고, vLLM의 버그임을 확인했습니다. 이미 다른 사람들이 리포트한 문제였고, 해당 이슈에서는 해결할 수 없다는 답과 함께 워크어라운드가 제시돼 있어서 그대로 적용했습니다.

결과는 에러율이 약 0.2%에서 0.02% 수준으로 감소했습니다.

사건 3. 사라진 처리량

트래픽이 몰리는 시간에 처리량이 부족해서 밀린다고 서버 개발자분이 그래프와 함께 메시지를 주셨어요.

LLM은 GPU라는 비싼 자원을 다량 쓰는 모델이라, 파드를 하나 더 띄우는 것 자체가 비쌌어요. 그래서 증설을 결정하기 전에 하나만 확인하고 가자고 했습니다.

우리가 GPU를 충분히 쓰고 있는가?

메트릭으로 돌아갔어요. 크게 보면 문제가 없어 보였는데, 자세히 보니 KV Cache 사용량이 매우 작았습니다. 생성 TPS는 거의 1,000에 가깝고 Prefix Cache도 아주 많이 쓰고 있어서 전반적으로 조밀하게 돌고 있었는데, KV 캐시 사용량만 1%도 되지 않는 수준이었어요.

메모리에 여유가 있으니 더 많은 요청을 받을 수 있겠다고 판단했습니다. 파드를 증설하기 전에 어디까지 동시성(Concurrency)을 받을 수 있는지 테스트해봤어요. 허용 범위 안에서 "동시성 20까지"까지 받을 수 있다는 걸 확인했습니다.

이 근거로 자원을 두 배로 쓰지 않고 트래픽을 4배까지 받을 수 있다는 힌트를 얻었어요.

그래도 다 해결되지는 않아요

물론 지표가 있다고 모든 문제가 풀리는 건 아닙니다. 문제 대부분이 저희가 의존하는 vLLM·SGLang 같은 프레임워크에 디펜던시가 걸려 있거든요.

  • 특정 옵션 조합에서 content 값이 아예 반환되지 않는 버그
  • 저희가 설정한 대로 LLM이 동작하지 않는 경우
  • 릴리스 노트에 "이 문제를 해결했다"고 적혀 있어 적용했더니, 오히려 CPU 스로틀링이 걸려 느려지는 부작용

그래서 미리 다 파악하는 방식으로 가지 않아요. 여러 대시보드로 문제를 파악하고, 탐색을 통해 원인을 발견하는 형태로 대응합니다.

문제를 다루는 네 단계

1. 발견. 중앙화된 모니터링과 로깅으로 타임아웃, finish_reason으로 인한 생성 중단, 레이턴시 급증, 메모리 포화 같은 신호를 탐지하고 알림을 받습니다.

2. 관찰. 알림을 받으면 지표로 들어가 문제를 관찰해요. 메트릭으로 이상치를 못 찾으면 Kibana 등에서 개별 로그를 확인합니다.

3. 가설과 검증. "이런 경우에 문제가 생기고 이런 경우엔 안 생기니 이렇게 해보자"는 가설을 세우고, 변경한 옵션·모델·설정을 ML 플랫폼으로 배포해 스트레스 테스트로 검증합니다.

4. 반영과 롤백. 서비스에 반영하되, 예상 못 한 부작용에 대비해 빠르게 롤백할 수 있도록 시스템을 갖춰둡니다.

마지막으로

LLM의 세계는 너무 빨리 변합니다. 모델만 바뀌는 게 아니라 서빙 프레임워크, 서빙 옵션, 호출 방법까지 계속 바뀌어요. 이걸 전부 따라가는 건 현실적으로 불가능합니다.

그래서 저희는 모두 따라가는 방식 대신, 최대한 많은 지표를 펼쳐두고 문제를 빨리 파악해 원인을 추정하고 빠르게 대응하는 시스템을 갖추기로 했어요.

여러 지표를 두고 동적으로 빠르게 대응할 수 있는 시스템을 만드는 것. 그게 LLM을 그냥 서빙하는 것과 잘 서빙하는 것 사이의 간극을 메우는 일이라고 생각합니다.


✅ 이번 아티클은 토스증권 Tech Talk Talk 세션을 바탕으로 재구성되었습니다.

뉴스레터가 발행되면
이메일로 알려드릴게요
구독하기
LLM 서빙, 띄우는 것과 잘 띄우는 것 사이