혼자 쓰는 챗봇의 현실 — OOM·동시성·테스트, 그리고 배운 것
이 챗봇은 클라우드 어딘가가 아니라 제 노트북 위에서 돌아갔습니다. 저녁에 터미널을 열어 uvicorn으로 서버를 띄우고, 브라우저에서 localhost:8000을 열면 그날의 AI 고객이 저를 기다리고 있었습니다. 화려한 인프라도, 관제 대시보드도 없었습니다. 임베딩 모델은 CPU에서 돌았고, 대화 이력은 로컬 파일에 쌓였고, 어떤 날은 로컬 모델을 몇 번 연달아 부르다 노트북 팬이 요란하게 돌기도 했습니다. 이 마지막 편은 그 "혼자 쓰는 도구"를 실제로 굴리며 마주친 현실 — OOM과 동시성, 그걸 막으려 짠 테스트, 그리고 개인 도구라서 오히려 가능했던 선택들 — 을 회고하고, 열 편으로 이어 온 이 구축기를 마무리하는 글입니다. 회고에 앞서 미리 밝혀 두면, 이 도구는 개인 학습용으로 직접 만든 것이고 글에 담은 시나리오는 실제 고객 미팅을 익명화·재구성한 것이라, 2026년 7월 시점 기준으로 특정 회사·고객사의 정보나 계약 수치는 등장하지 않습니다.
[시리즈] S2S·LLM 음성 에이전트 엔지니어링 63편 · 현재 10-8편
← 이전: 10-7 대화가 끝나면 채점한다 / 다음 → 11-1 KMS 프로젝트가 실사 이후로 밀리는 이유
- 10-6 퀴즈 풀 사전생성
- 10-7 대화가 끝나면 채점한다
- 10-8 혼자 쓰는 챗봇의 현실 (현재 글)
- 11-1 KMS 프로젝트가 실사 이후로 밀리는 이유
- 11-2 비정형 이력 데이터 지식화의 함정
전체 목차 펼치기 (63편)
Part 1 — 음성 AI가 느린 이유: 레이턴시 진단 (4편)
- 1-1 AI 보이스봇이 답답한 이유 — Cascaded 레이턴시
- 1-2 1초의 벽 — 지연이 UX를 결정하는 이유
- 1-3 지연 줄이기 — 스트리밍·청크로 TTFT 단축
- 1-4 RAG 병목과 오디오 버퍼링 제어
Part 2 — 텍스트를 넘어: End-to-End S2S의 원리 (4편)
- 2-1 End-to-End S2S 작동 원리
- 2-2 OpenAI Realtime API vs 오픈소스 S2S
- 2-3 음성 토큰(Audio Token)이란
- 2-4 감정·톤 조절이 CS를 바꾸는 이유
Part 3 — 실시간 대화와 에이전트화: 인터럽트·MCP·도구 (5편)
- 3-1 실시간 인터럽트(끼어들기) 감지
- 3-2 실시간 음성에 MCP가 필요한 이유
- 3-3 스트리밍 중 MCP 초고속 데이터 룩업
- 3-4 통화 중 외부 API 도구 호출
- 3-5 실시간 세션 관리 — 통화 맥락·고객 데이터
Part 4 — 인프라·세션·보안: 프로덕션 백엔드 (5편)
- 4-1 동적 컨텍스트 윈도우 관리
- 4-2 WebRTC·WebSocket 초저지연 음성
- 4-3 비동기 이벤트 루프와 동시 콜 처리
- 4-4 트래픽 스파이크를 견디는 확장성 설계
- 4-5 실시간 음성 보안 — 암호화·PII 마스킹
Part 5 — 운영·품질·평가: 에이전틱 워크플로우와 신뢰성 (7편)
- 5-1 에이전틱 워크플로우 — 응대를 넘어 완결로
- 5-2 상용화의 한계와 트러블슈팅
- 5-3 보이스 에이전트 평가(Eval) 설계
- 5-4 관측성(Observability) — 어디서 끊기나
- 5-5 가드레일과 환각 제어
- 5-6 상담사 전환(Handoff) 설계
- 5-7 프롬프트 A/B 테스트와 튜닝
Part 6 — 음성 경험 고도화·실전 통합: 한국어·UX·전화망·엣지 (5편)
- 6-1 한국어 음성 AI의 난관 — 존댓말·코드스위칭
- 6-2 턴테이킹과 시맨틱 VAD
- 6-3 보이스 페르소나 디자인
- 6-4 SIP·PSTN 전화망 연동
- 6-5 온디바이스·엣지 S2S와 프라이버시
Part 7 — S2S 도입 전략·구매 의사결정 (4편)
Part 8 — LLM 콜봇 PoC 현장 (6편)
- 8-1 LLM 콜봇 PoC — 검증의 갭
- 8-2 STT 정확도 93% 목표가 부족한 이유
- 8-3 금융 콜봇에서 소수점이 중요한 이유
- 8-4 TTFT 1.5초, 콜봇 스트리밍 파이프라인 설계
- 8-5 콜봇 SLM: 30B vs 70B 트레이드오프
- 8-6 PoC와 본사업 사이의 성능 괴리
Part 9 — 상담어시스트: 상담사 화면을 바꾸는 실시간 AI (7편)
- 9-1 상담어시스트란 무엇인가 — 실시간 STT가 상담사 화면을 바꾸는 구조
- 9-2 상담어시스트의 뼈대 — STT·인텐트·KMS 파이프라인
- 9-3 추천의 근거 — KMS·RAG 지식 그라운딩
- 9-4 통화 중 놓친 고지를 잡는 실시간 필수 안내 체크
- 9-5 AI가 상담 품질을 자동 채점하는 법 — AutoQA
- 9-6 신입 상담사에게 어시스트가 더 필요한 이유
- 9-7 온프레미스 상담어시스트 — GPU 제약과 모델 선택
Part 10 — 직접 만든 롤플레이 챗봇: RAG 에이전트 구축기 (8편)
- 10-1 왜 롤플레이 챗봇을 직접 만들었나
- 10-2 전체 아키텍처 한 장
- 10-3 RAG 파이프라인과 로컬 임베딩
- 10-4 멀티 LLM 백엔드 스위칭
- 10-5 AI 고객 페르소나 프롬프트 엔지니어링
- 10-6 퀴즈 풀 사전생성
- 10-7 대화가 끝나면 채점한다
- 10-8 혼자 쓰는 챗봇의 현실 (현재 글)
Part 11 — KMS 실무 — 지식관리시스템의 설계와 운영 현실 (8편)
로컬로 굴린다는 것 — OOM과 동시성
로컬에서 무언가를 돌린다는 건 낭만적으로 들리지만, 실제로는 자원의 한계와 매일 협상하는 일이었습니다. 임베딩은 CPU에서 돌아가는 작은 다국어 모델을 썼기 때문에 큰 문제가 없었습니다. 진짜 말썽은 언어 모델 쪽이었습니다. 여러 백엔드를 붙여 두었는데, 그중 로컬에서 도는 모델을 쓰다가 처음으로 서버가 통째로 죽는 일을 겪었습니다.
원인은 동시성이었습니다. 롤플레이 한 턴을 처리하는 동안에도 힌트를 생성하거나, 앞 턴을 채점하거나, 다음 발화를 준비하는 등 여러 LLM 호출이 겹칠 수 있었습니다. 클라우드 API라면 그저 요청이 몇 개 더 나가는 정도로 끝나지만, 로컬 모델은 호출 하나하나가 실제 메모리를 크게 잡아먹습니다. 동시 호출이 몇 개만 겹쳐도 메모리가 바닥나 OOM(메모리 부족)으로 프로세스가 강제 종료됐습니다. 연습을 하다 말고 서버가 사라지는 경험은, 도구에 대한 신뢰를 순식간에 깎아먹었습니다.
해결은 의외로 담백했습니다. asyncio.Semaphore로 동시에 실행되는 LLM 호출 수에 상한을 걸었습니다. 상한값은 환경 변수로 뺐지만 기본은 다섯으로 두었고, 모든 비동기 LLM 호출은 이 세마포어를 통과해야만 실행되도록 감쌌습니다. 여섯 번째 요청이 들어오면 앞선 호출 하나가 끝날 때까지 얌전히 기다립니다. 결과적으로 로컬 모델이 감당할 수 있는 만큼만 동시에 돌게 되면서, 그 이후로 OOM으로 죽는 일은 사라졌습니다. 화려한 큐 시스템도, 별도 워커도 필요 없었습니다. 혼자 쓰는 도구의 부하에서는 표준 라이브러리의 세마포어 한 줄이면 충분했습니다. 이 세마포어를 넣게 된 배경은 멀티 백엔드를 다룬 앞선 편에서 조금 더 자세히 이야기했습니다.
pytest 스위트로 지킨 것
혼자 쓰는 도구에 테스트를 넣는 게 과한 것 아니냐고 물을 수 있습니다. 저도 처음엔 그렇게 생각했습니다. 하지만 이 도구는 기능이 서로 얽혀 있어서, 한쪽을 고치면 예상치 못한 곳이 조용히 부서지는 일이 반복됐습니다. 시나리오 데이터의 형식을 조금 바꿨더니 채점이 엉뚱한 결과를 내고, 사용자 기록을 저장하는 구조를 손봤더니 예전 데이터를 못 읽는 식이었습니다. 문제는 이런 고장이 즉시 드러나지 않고, 며칠 뒤 연습하다가 발견된다는 점이었습니다.
그래서 핵심 로직마다 pytest 테스트를 붙여 나갔습니다. 거창한 커버리지 목표가 있었던 건 아니고, "한 번 데인 곳"에 하나씩 안전망을 치는 방식이었습니다. 시간이 지나며 테스트가 지키는 영역이 자연스럽게 몇 갈래로 나뉘었습니다.
- 시나리오·페르소나: 롤플레이의 재료가 되는 시나리오 세트와 페르소나 조합이 항상 유효한 형태로 로드되는지. 여기가 깨지면 대화 자체가 시작되지 않습니다.
- 채점(스코어링): 체크포인트 기반 채점이 필수 항목 적중을 제대로 반영하는지, 응답 스코어링이 기대한 구조로 나오는지. 채점은 이 도구의 핵심 가치라 가장 신경 쓴 영역입니다.
- 코칭·피드백: 종료 후 종합 피드백에서 코칭을 파싱하는 부분이, 모델 출력이 조금 흔들려도 안전하게 처리되는지.
- DB 마이그레이션: 저장 구조를 바꿔도 기존 데이터가 유실 없이 넘어오는지. 연습 이력이 사라지면 도구를 쓸 이유가 없어지기 때문에, 마이그레이션 회귀는 특히 단단히 막았습니다.
테스트는 이 도구에서 "느린 실패"를 "빠른 실패"로 바꿔 주는 장치였습니다. LLM 호출이 실제로 들어가는 느린 테스트에는 별도 마커를 달아 평소엔 건너뛰고, 형식·로직만 검증하는 빠른 테스트는 코드를 만질 때마다 돌렸습니다. 덕분에 손대면 부서지던 도구가, 손대도 안심할 수 있는 도구로 조금씩 바뀌어 갔습니다.
개인 도구지만, 최소한의 것들
이 도구는 저 혼자 쓰는 것이었지만, 그렇다고 아무것도 잠그지 않은 건 아니었습니다. 연습 이력과 점수는 본인만 열람한다는 원칙을 이 시리즈 내내 강조했는데, 그 원칙이 성립하려면 최소한 "누가 나인지"는 구분되어야 했습니다. 그래서 인증을 넣었습니다.
다만 개인 도구에 맞는 최소한의 형태였습니다. 비밀번호는 평문으로 저장하지 않고 bcrypt로 해시해서 보관했고, 로그인 시 입력값을 저장된 해시와 대조해 검증하는 정도였습니다. 화려한 세션 관리나 외부 인증 연동은 없었습니다. 하지만 "점수는 본인만 본다"는 이 도구의 존재 이유를 지키려면, 비밀번호를 평문으로 두는 것만큼은 하지 말아야 한다고 봤습니다. 과하지도, 부족하지도 않게 — 개인 도구에 필요한 딱 그만큼의 보안이 목표였습니다.
이런 판단은 인증뿐 아니라 곳곳에서 반복됐습니다. 지식 소스는 민감한 원문이 아니라 비식별 요약본을 썼고, 롤플레이 시나리오는 실제 미팅을 익명화·재구성한 것만 넣었습니다. 개인이 로컬에서 굴리는 도구일수록, 애초에 민감한 데이터가 도구 안으로 들어오지 않게 하는 편이 어떤 보안 장치보다 확실하다고 판단했습니다.
"혼자 쓰는 도구라서" 가능했던 선택들
돌이켜 보면, 이 도구의 많은 기술적 선택은 "혼자 쓰는 도구"라는 조건에서 나왔습니다. 그리고 그 조건은 제약이라기보다 오히려 자유에 가까웠습니다.
먼저 완벽 대신 빠른 실사용을 택할 수 있었습니다. 여러 사람이 쓰는 서비스였다면 동시성 이슈부터 인증·확장성까지 미리 다 갖춰 놓아야 했겠지만, 사용자가 저 하나였기에 일단 돌려 보고 막히는 지점을 그때그때 뚫는 방식이 가능했습니다. OOM도 겪고 나서 세마포어로 막았고, 테스트도 데인 곳부터 하나씩 쳤습니다. 미리 완벽하게 설계하려 했다면 애초에 시작조차 못 했을 겁니다.
더 흥미로운 건, "평가가 아니라 연습"이라는 이 도구의 철학이 기술 선택에까지 영향을 줬다는 점입니다. 평가 도구였다면 점수의 정확성·일관성·감사 추적이 최우선이었을 테고, 그러면 채점 로직도, 저장 구조도, 인증도 훨씬 무거워졌을 겁니다. 하지만 이건 연습 도구였기에, 채점은 "유일한 정답"이 아니라 "핵심을 짚었는가"를 보는 방향으로 갈 수 있었고, 퀴즈는 매번 생성하는 대신 미리 만들어 둔 풀에서 즉시 꺼내 레이턴시를 없앨 수 있었습니다. 지연을 줄이고 반복을 쉽게 만드는 쪽이, 완벽하게 엄정한 채점보다 이 도구엔 더 중요했습니다. 도구의 목적이 기술 스택의 무게를 정해 준 셈입니다.
시리즈를 마치며
이 시리즈는 "왜 직접 만들었나"에서 시작해 아키텍처, RAG 파이프라인, 멀티 LLM 백엔드, 페르소나 프롬프트, 퀴즈 사전생성, 채점 설계를 거쳐 여기 운영 회고까지 왔습니다. 열 편을 관통하는 하나의 문장을 꼽으라면, 이 도구는 "잘 만든 제품"을 목표로 한 적이 없다는 것입니다. 처음부터 끝까지, 온보딩의 가장 느린 구간을 개인이 압축하기 위한 연습 도구였고, 모든 기술 결정은 그 목적에 봉사했습니다.
RAG를 붙인 것도 정확도 자체가 목적이 아니라 현장에서 통하는 연습을 만들기 위해서였고, 점수를 본인만 보게 잠근 것도 마음 편히 틀릴 수 있어야 반복하기 때문이었습니다. 로컬로 굴린 것도, 세마포어로 동시성을 막은 것도, 테스트로 회귀를 잡은 것도 결국은 "매일 부담 없이 켜서 연습할 수 있는 도구"를 유지하기 위한 선택이었습니다. 만들며 배운 것들을 마지막으로 정리하면 다음과 같습니다.
- 목적이 스택을 정한다. 평가가 아니라 연습이라는 한 문장이 채점·저장·인증·레이턴시 설계를 전부 가볍게 만들었습니다. 기술을 먼저 고르면 목적이 흔들립니다.
- 수용성이 곧 생존이다. 아무리 정교해도 사람들이 켜지 않으면 0입니다. 점수를 본인만 보게 한 결정이 이 도구를 살렸습니다.
- 로컬의 한계는 담백한 해법으로 넘는다. OOM은 거창한 인프라가 아니라 세마포어 한 줄로 잡혔습니다. 혼자 쓰는 규모에선 표준 라이브러리로 충분한 경우가 많습니다.
- 개인 도구에도 테스트는 값을 한다. "느린 실패"를 "빠른 실패"로 바꾸는 안전망은, 손대면 부서지던 도구를 손대도 안심되는 도구로 바꿔 줍니다.
- 완벽보다 실사용이 먼저다. 일단 돌려 보고 막히는 곳을 뚫는 순서였기에 이 도구는 실제로 존재하게 됐습니다. 미리 완벽했다면 시작조차 없었을 겁니다.
직접 만든 도구를 매일 켜서 쓰는 경험은, 기술 문서로는 얻기 힘든 감각을 남겼습니다. 무엇이 진짜 필요하고 무엇이 군더더기인지는, 만들어서 써 봐야 비로소 갈립니다. 이 열 편이 자신만의 연습 도구를 만들어 보려는 누군가에게 작은 참고가 되었길 바라며, 롤플레이 에이전트 구축기를 여기서 마칩니다.
댓글
댓글 쓰기