이 특집의 클라이맥스. 넷플릭스는 모델이 '아마도 유효한' JSON을 뱉고 나서 검사하는 대신, 애초에 규칙을 어길 수 없도록 생성 과정 자체를 제약한다. 이 제약 디코딩이 왜 필요하고 어떻게 작동하는지, 그리고 그것을 '규모'에서 돌리려다 vLLM V0의 GIL 벽에 부딪혀 V1으로 넘어간 넷플릭스의 실전 분투기를 따라간다. 직접 서빙해야만 가능한, 커스터마이징의 끝판왕.
코어닷투데이2026-07-3028분
커스터마이징의 끝판왕
1부에서 자체 서빙의 다섯 이유 중 ④ 커스터마이징을 "이번 특집의 진짜 주인공"이라 예고했다. 2부에서는 response_format이라는 이름으로 그 주인공의 그림자를 봤다. 이제 3부에서 정체를 밝힌다. 이름은 제약 디코딩(constrained decoding). API로는 절대 못 하고, 오직 생성 과정을 직접 소유해야만 가능한 기술이다.
실무에서 LLM을 쓰는 흔한 방식을 보자. 우리는 종종 모델에게 정해진 형식으로 답하라고 요구한다. 예를 들어 "제목·평점·태그를 JSON으로 뽑아줘" 같은 것이다. 그래야 그 답을 다음 프로그램이 받아 처리할 수 있으니까.
문제는 LLM이 이 약속을 자주, 그러나 예측 불가능하게 어긴다는 것이다. 쉼표 하나를 빠뜨리고, 따옴표를 안 닫고, 요청하지도 않은 설명을 앞에 붙인다. 그래서 전통적인 방식은 이렇게 돌아간다.
호출
모델을 부른다 — "JSON으로 답해줘"
기대
유효한 JSON이 오길 기도한다
파싱
파싱 시도 → 가끔 실패 (쉼표·따옴표·군더더기)
수리/재시도
고쳐보거나 모델을 다시 호출 → 처음으로 되돌아감
이 루프의 대가는 뼈아프다. 재시도 한 번은 모델 호출 한 번을 통째로 다시 하는 것이다. 지연이 늘고, 토큰 비용이 또 든다. 말 그대로 무효한 생성값에 돈을 지불하고, 그걸 버리고, 다시 시도하는 것이다. 에이전트처럼 여러 단계가 사슬로 엮인 경우, 중간에 JSON 하나가 깨지면 사슬 전체가 끊기고 그 비용은 단계마다 복리로 불어난다.
발상의 전환: 어길 수 없게 만든다
넷플릭스의 해법은 발상 자체가 다르다.
"사후에 검사하지 말고, 애초에 규칙을 어길 수 없게 만들자."
비유하자면, 맞춤법 틀린 글을 쓰게 두고 빨간 펜으로 고치는 게 아니라 — 애초에 틀린 글자는 키보드에서 눌리지 않게 하는 것이다. 커브 없는 레일 위의 기차가 절대 탈선할 수 없듯, 제약 디코딩을 건 모델은 구성상(by construction) 형식을 어길 수가 없다. 검사도, 재시도도, 수리도 필요 없다. 유효한 출력이 처음부터 보장된다.
✗
사후 검증 (기존)
생성 → 검사 → 실패 → 재시도(재과금). 유효성은 '운'에 맡긴다.
✓
제약 디코딩 (넷플릭스)
생성 도중에 규칙을 강제 → 무효한 토큰은 아예 선택 불가. 유효성이 보장된다.
어떻게 어길 수 없게 만드나: 토큰 마스크
마법의 원리는 의외로 단순하다. 2부에서 배운 것을 떠올리자. LLM은 답을 토큰 하나씩 이어 쓴다. 매 스텝, 모델은 "다음에 올 수 있는 모든 후보 토큰"에 점수(logit)를 매기고 그중 하나를 고른다.
제약 디코딩은 바로 이 '고르기 직전'에 개입한다. 규칙상 지금 나오면 안 되는 토큰들의 점수를 음의 무한대(−∞)로 깔아뭉갠다. 그러면 그 토큰은 절대 선택되지 못한다. 이 '허용/금지' 명단을 토큰 마스크라 부른다.
JSON을 생성하는 중이라고 하자
현재까지
{ "rating":
규칙
지금은 숫자나 따옴표가 와야 한다. 닫는 중괄호 }나 문자 abc는 문법 위반.
마스크
허용: 4, 5, "… | 금지(−∞): } , abc …
그렇다면 "지금 무엇이 허용되는가"는 누가 판단할까? 여기서 상태 머신(state machine) 이 등장한다. 우리가 원하는 형식(JSON 스키마, 정규식, 문법)을 하나의 자동기계로 컴파일한다. 이 기계는 현재 어느 '상태'에 있는지 안다 — "방금 여는 중괄호를 봤으니 다음엔 키(문자열)가 와야 해", "방금 콜론을 봤으니 다음엔 값이 와야 해" 같은 식이다. 매 스텝, 기계의 현재 상태가 허용 토큰 집합을 결정하고, 모델이 토큰을 하나 고르면 기계는 다음 상태로 전진한다.
1
원하는 형식을 상태 머신(FSM/문법)으로 컴파일
2
매 스텝, 현재 상태가 허용 토큰 마스크를 만든다
3
금지 토큰의 점수를 −∞로 → 모델은 허용 토큰만 고름
4
고른 토큰만큼 상태 머신이 전진 → 완성될 때까지 반복
백문이 불여일견. 아래에서 상태 머신이 JSON을 만들며 어떻게 토큰을 열고 닫는지 직접 스텝을 밟아보자.
이 아이디어의 계보
이 '마스크를 싸게 만드는' 경쟁에서 몇 가지 유명한 라이브러리가 나왔다. 넷플릭스가 직접 만든 건 아니지만, 개념을 이해하는 데 도움이 된다.
라이브러리
핵심 아이디어
대표 성과
Outlines (2023)
형식을 유한 상태 기계(FSM)로 보고, 상태→허용토큰 색인을 미리 계산
매 스텝 어휘 전체를 훑지 않아 사실상 O(1)
XGrammar (2024)
토큰을 '맥락 무관/맥락 의존'으로 나눠 앞엣것은 미리 캐싱
기존 대비 최대 ~80× 빠른 구조화 생성
LM Format Enforcer
문자 단위 파서 + 어휘 접두어 트리의 교집합
부분 단어 토큰까지 정확히 처리
공통의 어려움은 이것이다. 이 마스크를 어휘(수만~수십만 토큰) 전체에 대해, 매 스텝, 모든 요청마다 만들어야 한다. 이게 얼마나 무거운 일인지가 — 바로 넷플릭스가 규모의 벽에 부딪힌 지점이다.
규모의 벽: 벤치마크엔 안 보이던 병목
넷플릭스는 제약 로직을 vLLM의 커스텀 로짓 프로세서(logits processor) 로 구현했다. 2부에서 본, "고르기 직전에 개입하는" 바로 그 훅이다. 각 요청은 자기만의 규칙을 가지므로, 요청마다 자기 상태 머신을 하나씩 붙인다. 순수 파이썬으로 짠 첫 구현은 기능적으로는 완벽히 작동했다. 하지만 프로덕션 규모에서 병목이 터졌다.
문제를 이해하려면 vLLM의 첫 세대 아키텍처, V0의 작동을 봐야 한다.
GPU가 배치 전체의 logit을 계산
CPU가 그걸 넘겨받아 복사·대기
요청 1개씩 순차적으로 제약 로직 실행
다 끝나면 샘플링
문제는 세 번째 칸의 '순차적으로' 다. 왜 순차일까? 파이썬에는 GIL(Global Interpreter Lock) 이라는 족쇄가 있다. 한 번에 하나의 스레드만 파이썬 코드를 실행하게 막는 잠금장치다. 그래서 요청 32개가 배치에 있어도, 각 요청의 제약 로직이 한 줄로 서서 차례를 기다린다.
배치 8개
CPU 시간 ×8
배치 16개
CPU 시간 ×16
배치 32개
CPU 시간 ×32
결과는 치명적이다. 로짓 처리의 CPU 시간이 배치 크기에 정비례해 늘어난다. GPU는 배치를 통째로 효율적으로 처리하는데, 정작 그 앞단의 CPU 제약 로직이 요청 수만큼 선형으로 부풀어 전체 지연을 잡아먹는다. 최종 지연이 GPU가 아니라 CPU에 묶여버린(CPU-bound) 것이다.
가장 고약한 점: 이 병목은 요청 하나만 던지는 벤치마크에선 절대 안 보인다. 요청이 하나면 '순차 처리'할 것도 없으니까. 오직 현실적인 동시 부하가 걸릴 때만 꼬리 지연(tail latency)으로 터진다. 설계 단계에서 예측하기 가장 어려운 종류의 함정이다.
⚠️ 정확히 짚자. vLLM 커뮤니티가 배치 레벨 설계로 옮긴 1차적 명분은 문서상 "벡터화(vectorization)"다. 다만 그 밑바탕의 메커니즘이 바로 이 GIL로 인한 파이썬 순차 실행이며, 넷플릭스도 실전에서 정확히 이 CPU 선형 증가를 병목으로 지목했다.
두 세대의 차이를 직접 굴려보자. 배치 크기를 키우면 무슨 일이 벌어지는지.
벽을 넘다: vLLM V1의 배치 레벨 설계
구조적 해결은 vLLM V1에서 왔다. V1은 2025년 초 공개된 vLLM 코어의 대대적 재설계다. 1년 반 동안 기능들이 따로따로 자라 얽힌 기술 부채를 걷어내고, 스케줄러·KV 캐시 관리자·샘플러를 밑바닥부터 다시 지었다. 그 핵심 변화 중 하나가 로짓 프로세서를 요청 단위에서 배치 단위로 옮긴 것이다.
V0 (요청 단위)
V1 (배치 단위)
실행 단위
요청마다 파이썬 콜백 1회씩
배치 전체 텐서를 한 번에
확장성
CPU 시간이 배치에 선형 증가
배치가 커져도 평탄
구현
순수 파이썬 (GIL에 묶임)
핫패스를 C++ 멀티스레딩으로
상태 관리
요청별로 단순
배치 변화를 명시 추적해야 (복잡)
넷플릭스는 커스텀 프로세서를 배치 레벨 자료구조 위에서 작동하도록 다시 썼다. 여러 요청의 마스크를 한꺼번에 계산하고, 가장 뜨거운 경로를 C++ 멀티스레딩으로 재구현해 GIL을 우회했다. 그 결과, 앞의 시뮬레이터에서 봤듯 배치가 커져도 로짓 처리 시간이 평탄하게 유지된다. 규모의 벽을 넘은 것이다.
공짜는 아니었다. V1 방식은 배치 멤버십 변화를 명시적으로 추적해야 한다. 연속 배칭(2부) 때문에 배치 구성은 매 스텝 바뀐다 — 누가 나가고, 누가 들어오고, 자리가 어떻게 옮겨졌는지. V1 인터페이스는 매 스텝 update_state(batch_update)로 이 변화를 넘겨받아, 각 요청의 상태 머신이 배치 안 자기 자리를 계속 정확히 따라가게 한다. V0의 요청별 인터페이스보다 복잡하지만, 동적으로 변하는 배치에서 상태를 정확히 유지하려면 필수다.
마지막 두 유령: 부분 프리필과 선점
성능은 해결됐다. 하지만 "생성 도중에 상태를 들고 다니는" 제약 로직은, 설계 단계에서 아무도 예상 못 한 두 개의 유령을 불러냈다. 둘 다 2부에서 배운 개념이 되돌아와 문 것이다.
👻
유령 ① 부분 프리필 (partial prefill)
2부의 청크 프리필을 기억하는가. V1은 긴 프롬프트를 여러 스텝에 나눠 프리필한다. 그런데 배치 업데이트 신호만으로는 "이 요청이 완전히 프리필됐는지, 반만 됐는지"를 구분할 수 없었다. 상태 머신이 엉뚱한 시점에 전진할 위험 → 넷플릭스는 내부 추적 장치를 따로 붙여 해결.
👻
유령 ② 선점 (preemption)
2부의 메모리 압박을 기억하는가. GPU 메모리가 부족하면 vLLM은 진행 중인 요청의 KV 캐시를 내쫓았다가 나중에 다른 프롬프트·토큰 목록으로 다시 불러올 수 있다. 이건 "출력 토큰 목록은 계속 늘어나기만 한다"는 상태 머신의 대전제를 깨뜨린다. → 토큰 이력이 줄어드는 순간을 감지해 상태 머신을 리셋하고 새 프롬프트로 재초기화.
이 두 유령이 주는 교훈은 특집 전체를 관통한다. 화려한 알고리즘은 시작일 뿐이고, 진짜 엔지니어링은 이런 경계 조건(edge case)에서 완성된다. 청크 프리필과 선점은 각각 처리량과 메모리를 위한 훌륭한 최적화지만, 상태를 들고 다니는 제약 디코딩과 만나는 순간 예상 못 한 상호작용을 낳는다. 이 미묘한 얽힘을 하나씩 풀어낸 것이 넷플릭스 플랫폼을 진짜 견고하게 만들었다.
정직한 한계: 제약 디코딩이 못 하는 것
균형을 위해 반드시 짚어야 할 것. 제약 디코딩은 만능이 아니다.
과잉 주장 금지
한계 1
무효(invalid)는 막아도, 틀린-유효(wrong-valid)는 못 막는다. 형식은 완벽한 JSON이지만 내용이 틀린 답은 여전히 나올 수 있다. "형식 보장 ≠ 정답 보장".
한계 2
제약세(constraint tax). 모델이 정말 쓰고 싶어 한 토큰이 형식상 금지되면, 억지로 다른 길로 몰려 내용 품질이 떨어질 수 있다. 작은 모델일수록 이 세금이 크다는 연구들이 있다.
그럼에도 넷플릭스가 이 길을 택한 이유는 분명하다. 도구 호출·에이전트·구조화 추출·후보 제약 랭킹처럼 "형식이 깨지면 파이프라인 전체가 무너지는" 작업에서는, 무효 출력을 원천 봉쇄하는 이득이 제약세를 압도한다. 특히 넷플릭스의 추천처럼 출력을 유효한 후보 목록 안으로 강제해야 하는 경우, 제약 디코딩은 선택이 아니라 필수다.
넷플릭스가 다음에 볼 곳
원문은 앞으로의 투자 방향으로 글을 맺는다. 지금 마찰이 있는 곳들이다.
시스템 프롬프트 압축 품질을 지키며 프롬프트 길이를 줄여 프리필 부담을 낮춘다.
V1 비동기 스케줄링 스케줄링과 GPU 실행을 더 겹쳐 CPU 오버헤드를 더 숨긴다.
벡터화된 로짓 프로세서 CPU 코드가 아니라 GPU 융합 커널로 마스킹을 돌린다 — 이번 3부 병목의 최종 해법.
저정밀 모델 변형 더 낮은 정밀도로 메모리를 줄이고 처리량을 올린다(양자화).
특히 세 번째, 로짓 마스킹 자체를 GPU 커널로 올리겠다는 대목이 인상적이다. 이번 글의 전체 서사 — CPU 순차(V0) → CPU 배치 병렬(V1) → GPU 커널(다음) — 의 자연스러운 종착지이기 때문이다. 병목은 늘 한 계단 위로 올라간다.
특집을 마치며: 디테일이 곧 실력
3부에 걸친 넷플릭스 이야기가 우리에게 남기는 것은 무엇일까.
가장 화려한 교훈은 "직접 서빙하면 강력한 커스터마이징(제약 디코딩)이 가능하다"일 것이다. 하지만 진짜 교훈은 그 아래에 있다. 넷플릭스 원문 스스로 이렇게 고백한다 — "교훈은 대개 디테일에 있었다." 버전 핀, 조용히 사라진 API 필드, 포장 방식의 트레이드오프, GIL이라는 족쇄, 그리고 청크 프리필과 선점이라는 두 유령.
이것이 자체 서빙의 진짜 얼굴이다. API 한 줄이 감춰주던 이 모든 디테일을, 직접 서빙하는 순간 하나도 빠짐없이 마주하게 된다. 그리고 그 디테일 하나하나를 정직하게 풀어낸 축적이, 넷플릭스처럼 사반세기의 서빙 제국을 만든다.
★
코어닷투데이의 시선
모든 회사가 넷플릭스처럼 자체 서빙을 해야 하는 건 아니다. 오히려 대부분은 API가 정답이다. 하지만 "내 서비스의 규모·데이터·제어 요구가 어느 지점에서 그 선을 넘는가"를 아는 것 — 그리고 그때 마주할 디테일이 무엇인지 미리 아는 것 — 이야말로 AI 인프라를 다루는 팀의 진짜 실력이다. 이 특집이 그 지도가 되었기를 바란다.
3부 요약
제약 디코딩은 무효한 출력을 사후에 검사·재시도하는 대신, 생성 도중 규칙을 강제해 구성상 유효를 보장한다.
원리는 토큰 마스크: 상태 머신이 매 스텝 허용 토큰을 정하고, 금지 토큰 점수를 −∞로 눌러 선택 불가로 만든다. (Outlines·XGrammar·LM Format Enforcer 계보)
V0의 벽: 요청 단위 파이썬 로짓 프로세서가 GIL 탓에 순차 실행 → CPU 시간이 배치에 선형 증가 → 벤치마크엔 안 보이는 꼬리 지연.
V1의 돌파: 배치 레벨 재설계 + C++ 멀티스레딩으로 GIL 우회 → 배치가 커져도 평탄. 대신 update_state로 배치 변화를 명시 추적.
두 유령: 청크 프리필(부분 프리필 구분)과 선점(상태 머신 리셋)이라는 경계 조건을 따로 처리해야 견고해진다.
정직한 한계: 무효는 막아도 틀린-유효는 못 막고, '제약세'로 품질이 깎일 수 있다. 그럼에도 형식이 생명인 작업엔 필수.
참고 자료
· Netflix TechBlog, "In-House LLM Serving at Netflix" (2026-07-17) / InfoQ 정리본
· vLLM Blog, "vLLM V1: A Major Upgrade to vLLM's Core Architecture" (2025-01-27) 및 Logits Processors 설계 문서, RFC #13360
· Willard & Louf, "Efficient Guided Generation for LLMs" (arXiv:2307.09702, Outlines)
· Dong, Ruan et al., "XGrammar" (arXiv:2411.15100)
· "제약세/구조화 생성의 숨은 비용" 관련 arXiv 연구들. GIL 관련 서술은 본문 경고 참조.