Jiwon Kim

Cubi AI 음성 챗봇 플랫폼

2024.07 – 2025.03 · 백엔드 개발 (선임) · 엔쓰리엔(주) 직지랩스

  • Java 21
  • Spring Boot 3.3.5
  • Spring Cloud Gateway
  • Spring WebFlux
  • Redis
  • FFmpeg
  • NVIDIA Triton Inference Server
  • Gradle
  • Docker

Overview

개요

말로 대화하는 AI 챗봇 서비스의 백엔드입니다. 음성을 글자로 바꾸고(STT) → AI가 답을 생각하고(LLM) → 다시 음성으로 들려주는(TTS) 과정을 끊김 없이 이어 붙이고, AI 추론 서버(NVIDIA Triton)와 연동했습니다.

  • Spring Cloud Gateway 기반 AI 서비스 단일 진입점 — 경로 rewrite 라우팅 + 추론 타임아웃(504) 필터(JWT 없는 경량 AI 게이트웨이)
  • STT: FFmpeg로 16kHz 모노 PCM 변환 → 16-bit PCM을 float 정규화 → Triton 추론 → 텍스트 (멀티파트 업로드 리액티브 파이프라인)
  • LLM: 질문 + Redis 멀티턴 대화이력을 합쳐 Triton 추론 → 답변을 대화이력에 누적(fingerprint 세션 키)
  • TTS: 텍스트 → Triton 추론 → float 샘플을 16-bit PCM/RIFF·WAVE 헤더로 직접 인코딩 → audio/wav 스트리밍 응답
  • Triton KServe v2(/v2/models/{model}/infer) 추론 프로토콜 연동 + @JsonTypeInfo 다형 응답 역직렬화

Architecture

시스템 아키텍처

Cubi AI 음성 챗봇 아키텍처: Client(음성 + fingerprint) → Backend Service(Redis 세션) → Triton Inference Server(STT·LLM·TTS) → 응답 음성 반환 흐름
시스템 데이터 흐름

Metrics

성과 지표

STT→LLM→TTS

음성 루프

실시간 파이프라인

NVIDIA Triton

추론 연동

FFmpeg / PCM / WAV

미디어 처리

전·후처리 직접 구현

Troubleshooting

트러블슈팅

STT→LLM→TTS 음성 챗봇 파이프라인

문제 상황

음성 챗봇은 "음성 입력 → 텍스트화(STT) → 답변 생성(LLM) → 음성 합성(TTS) → 음성 출력"이라는 다단 추론 루프를 거쳐야 했다. 각 단계가 외부 추론 서버 호출에 의존해 지연이 크고, 단계 간 데이터 형식(오디오/텍스트/샘플)이 달라 블로킹 방식으로 묶으면 처리량이 떨어졌다.

원인 가설

각 단계를 마이크로서비스로 분리하고 게이트웨이 뒤에 두면 독립 확장이 가능하다. 단계별 처리를 WebFlux 풀리액티브 flatMap 체이닝으로 묶으면 스레드 블로킹 없이 "전처리 → 추론 → 후처리/영속"을 비동기로 오케스트레이션할 수 있다. 세션 식별자(fingerprint)를 전 단계에 공유하면 대화 맥락을 일관되게 유지할 수 있다.

검증

STT/LLM/TTS를 독립 서비스로 분리하고 게이트웨이가 경로 rewrite로 라우팅하는지, 각 서비스가 리액티브 파이프라인으로 입력→추론→출력을 논블로킹 처리하는지 확인했다. fingerprint가 STT/LLM/TTS/대화이력 전반의 세션 키로 동작하는지 점검했다.

해결

  • STT/LLM/TTS를 게이트웨이 뒤 마이크로서비스로 분리하고, 각 단계를 WebFlux 풀리액티브 파이프라인으로 오케스트레이션했다.
  • 게이트웨이는 경로 rewrite 라우팅 + 추론 응답 타임아웃(504) 필터를 적용했다(장시간 추론 대응, JWT 없는 경량 AI 게이트웨이).
  • fingerprint를 세션 식별 키로 STT/LLM/TTS/대화이력 전반에서 공유했다.

미디어 전처리/후처리 직접 구현 (FFmpeg / PCM / WAV)

문제 상황

추론 서버는 정규화된 오디오 텐서를 입력으로 받고, 합성 결과는 raw 오디오 샘플로 반환했다. 업로드된 음성은 코덱·샘플레이트가 제각각이라 추론 입력 규격(16kHz 모노 16-bit PCM)에 맞게 변환해야 했고, 합성 결과는 클라이언트가 재생할 수 있는 WAV로 인코딩해야 했다.

원인 가설

입력은 FFmpeg로 16kHz 모노 PCM으로 리샘플링한 뒤 16-bit PCM을 float로 정규화하면 추론 입력 규격을 맞출 수 있다. 출력은 추론이 돌려준 float 샘플을 16-bit PCM으로 변환하고 RIFF/WAVE 헤더를 직접 작성하면 표준 WAV로 만들 수 있다.

검증

업로드 파일을 FFmpeg ProcessBuilder로 -ar 16000 -c:a pcm_s16le 변환하고, WAV 44바이트 헤더를 스킵한 뒤 16-bit little-endian PCM을 /32768.0으로 float 정규화하는 STT 전처리를 검증했다. TTS는 추론 결과 float 샘플을 *32767 16-bit PCM(little-endian)으로 변환하고 RIFF/WAVE 헤더(샘플레이트/채널/비트)를 직접 작성하는지 확인했다. 임시 파일 정리와 단계별 예외 세분화도 점검했다.

해결

  • STT 전처리: FFmpeg 리샘플링(16kHz 모노 PCM) + 16-bit PCM→float 정규화를 직접 구현하고, 단계별 임시 파일 정리·예외를 세분화했다.
  • TTS 후처리: 추론 float 샘플 → 16-bit PCM 변환 → RIFF/WAVE 헤더 직접 인코딩 → audio/wav 스트리밍 응답을 구현했다.
  • (입력의 헤더 스킵/파싱과 출력의 헤더 생성은 방향이 반대인 별개 처리다.)

Triton 추론 서버 연동 (KServe v2 다형 응답)

문제 상황

실제 모델 추론은 자체 엔진이 아니라 외부 추론 서버(NVIDIA Triton)에 위임했다. 추론 요청/응답이 KServe v2 프로토콜의 JSON 텐서 포맷이고, 응답 출력이 모델에 따라 텍스트/오디오/언어/샘플레이트/이력 등으로 형태가 달라 단일 타입으로 역직렬화하기 어려웠다.

원인 가설

/v2/models/\{model\}/infer 요청 body를 입력 텐서(datatype/shape/data) 규격으로 구성하면 추론 호출을 표준화할 수 있다. 응답은 @JsonTypeInfo/@JsonSubTypes로 출력 종류별 다형 역직렬화하면 텍스트/오디오 등 이종 출력을 타입 안전하게 매핑할 수 있다.

검증

WebClient로 추론 엔드포인트에 POST하고 출력 필드(OUTPUT_TEXT/OUTPUT_AUDIO 등)를 추출하는 공통 호출 패턴을 검증했다. @JsonTypeInfo 기반 다형 응답 모델이 TEXT/AUDIO/LANG/SAMPLE_RATE/HISTORY 출력을 올바르게 역직렬화하는지 확인했다.

해결

  • Triton KServe v2(/v2/models/{model}/infer) 추론 프로토콜을 WebClient 논블로킹 호출로 연동했다(엔드포인트는 외부 주입).
  • 응답은 @JsonTypeInfo 다형 역직렬화로 이종 출력(텍스트/오디오/언어/샘플레이트/이력)을 타입 안전하게 매핑했다.
  • STT/LLM/TTS 공통의 추론 호출 패턴으로 일관성을 확보했다.

Redis 멀티턴 대화 컨텍스트

문제 상황

LLM이 자연스러운 대화를 하려면 이전 질문/답변 맥락이 필요했다. 그러나 추론 서버는 상태가 없어(stateless) 매 요청마다 과거 대화를 함께 전달해야 했고, 세션별로 이력을 누적·조회·초기화할 저장소가 필요했다.

원인 가설

세션 식별자(fingerprint)별로 Redis 리스트에 \{질문, 답변\}을 누적하면, 추론 요청 시 과거 이력을 함께 주입해 멀티턴 컨텍스트를 구성할 수 있다. 조회는 리스트 전체를 클라이언트 포맷으로 가공하고, 리셋은 키 삭제로 처리하면 된다.

검증

LLM 응답을 fingerprint 키의 Redis 리스트에 rightPush로 누적하고, 다음 추론 요청 시 그 이력을 입력에 함께 구성하는지 확인했다. 대화이력 조회(GET)가 리스트를 클라이언트 포맷으로 변환하고, 리셋(DELETE)이 이력 키 삭제 + 산출물 폴더 아카이빙으로 처리되는지 점검했다.

해결

  • Redis 리스트 기반 멀티턴 대화 컨텍스트(fingerprint 세션 키)를 누적/조회/리셋하도록 구현했다.
  • 추론 요청 시 과거 대화이력을 함께 주입해 맥락 있는 답변을 생성하도록 했다.
  • 세션 리셋 시 음성 산출물 폴더를 타임스탬프 기반으로 아카이빙해 데이터를 보존했다.