
RAG 시스템을 구축할 때 대다수의 시선은 자연스럽게 LLM 모델의 성능이나 프롬프트 엔지니어링으로 향합니다.
하지만 실무 환경에서 시스템을 안정적으로 운영하기 위해 먼저 치열하게 고민해야 하는 영역은 모델 뒷단에 위치한 백엔드 파이프라이라 생각합니다.
문서가 어떤 경로로 수집되고, 각 단계에서 실패했을 때 데이터 정합성을 어떻게 유지할 것인지가 시스템의 성패를 가른다고합니다.
RAG는 겉보기에 질의응답 기능처럼 보이지만, 백엔드 관점에서는 문서 수집, 비동기 이벤트 처리, 벡터 데이터베이스 적재, 실패 재처리, 그리고 관찰 가능성이 촘촘하게 얽힌 분산 데이터 파이프라인이라 생각들어서
이번에 제조 설비 매뉴얼을 대상으로 한 RAG 백엔드를 설계하며 고민했던 핵심 구조와 기술적 트레이드오프를 정리하기위해 포스팅하게되었습니다.
설계 대상 시스템은 수백 페이지 분량의 제조 설비 매뉴얼을 업로드하고, 사용자가 질문을 던지면 의미적으로 연관된 문서 조각을 찾아 답변을 생성하는 구조입니다.
초기 프로토타입 수준에서는 흐름이 단순하다. PDF를 업로드받아 텍스트를 추출하고, 임베딩 API를 거쳐 벡터 DB에 넣은 뒤 검색하는 방식 입니다.
하지만 운영 환경에서는 이 단선적인 흐름의 모든 구간이 잠재적 장애 포인트가 됩니다.
따라서 초기 설계부터 완벽한 예외 처리를 장담하기보다는, 어느 단계에서 실패했는지 명확히 추적하고 안전하게 재시도할 수 있는 격리된 파이프라인을 구축하는 것을 최우선 목표로 잡았습니다.
queued 상태를 반환하도록 격리했다. 실제 무거운 색인 작업은 별도의 컨슈머가 비동기로 처리합니다.PostgreSql나 검색 엔진인 Elasticsearch도 후보 입니다.pgvector는 관리가 편하지만, 향후 수많은 대형 설비 매뉴얼이 누적될 경우를 대비해 벡터 검색 엔진 자체를 독립적으로 스케일아웃할 수 있는 인프라가 필요했는데 Milvus는 고성능 분산 검색을 보장하는 대신, 내부적으로 etcd와 MinIO(로그스택도 나름괜찮음) 등을 함께 띄워야 하므로 관리해야 할 인프라 컴포넌트가 늘어나는 운영 비용 부담이 발생되어 로컬에서 할수있는것으로
분석 성능과 인프라 복잡도 사이의 비용 배분 결과로 Milvus를 채택했습니다.
선택: 외부 GPU 클라우드 독립 엔드포인트 활용했습니다. (특정 모 인프라는 잔고장? 설치하는데 시간낭비가 너무 심하고 흔히사용하는 인프라는 기본 사용량이 300만원이라 부담되기때문에)
이유와 트레이드오프: 내부 Docker Compose 환경에 vLLM을 묶어 띄우는 대안도 있었으나, 웹 애플리케이션과 LLM 서빙 서버는 자원 사용 패턴과 장애 특성이 완전히 다릅니다.
모델 추론 부하로 인해 추론 서버가 지연되더라도, 도메인 비즈니스 로직을 처리하는 백엔드 API와 문서 색인 파이프라인까지 동반 마비되는 현상을 막기 위해 서빙 레이어를 물리적으로 완전히 격리했다.
파이프라인의 생명주기를 데이터베이스상에서 명시적인 상태값으로 관리하였습니다.
[Document Uploaded] ──> status: "queued"
│
(Consumer Start)
▼
status: "indexing"
│
┌────────────┴────────────┐
(Success) (Failure)
▼ ▼
status: "indexed" status: "failed"
사용자는 내 문서가 단순히 업로드만 된 상태인지, 현재 파싱 중인지, 아니면 검색 엔진에 반영되어 즉시 질문 가능한 상태인지를 화면에서 정확히 인지할 수 있습니다.
운영자 역시 failed 상태로 멈춘 문서의 ID를 추적하여 실패 원인을 규명하고 타겟 재처리를 수행할 수 있습니다.
애플리케이션은 특정 LLM 라이브러리에 종속되지 않도록 내부 추상화 레이어를 거치며, 엔드포인트 URL과 API Key 환경변수만 바라봅니다.
이 구조 덕분에 모델을 로컬 테스트용 가벼운 모델에서 RunPod 기반의 고성능 오픈소스 모델, 혹은 상용 외부 API로 전환할 때 백엔드 코드의 수정이나 재배포 없이 인프라 설정 변경만으로 대응이 가능합니다.
네트워크 일시 지연 등으로 Kafka 메시지가 재처리될 때, 동일한 Chunk 데이터가 Milvus에 중복으로 insert되어 검색 품질을 망치는 문제가 발생할 수 있습니다.
이를 해결하기 위해 색인 프로세스 시작 시 doc_id 기준의 기존 벡터 데이터를 먼저 삭제한 후 삽입하도록 처리하여 여러 번 실행해도 동일한 결과가 나오는 멱등성을 확보했습니다.
컨슈머가 문서 파싱 중 처리 불가능한 예외를 만나 실패했을 때, 계속 해당 메시지에 붙잡혀 전체 파이프라인이 블로킹되거나 무한 루프에 빠지는 현상을 방지해야 합니다.
지정된 횟수 이상 실패한 이벤트는 에러 로그와 함께 Kafka DLQ 및 실패 관리용 비정형 저장소 로 격리한다.
RunPod 등 외부에 노출된 추론 엔드포인트의 네트워크 지연이나 콜드 스타트 현상으로 인해 사용자 응답이 무한정 늘어날 수 있습니다.
단순히 가혹한 타임아웃만 설정하면 긴 문맥을 처리할 때 정상적인 답변까지 끊기게 됩니다.
이를 방지하기 위해 스트리밍 응답 구조를 기본 채택하여 첫 토큰 반환 시간을 최소화하고, 외부 인프라 마비 시 시스템 전체 확산을 막기 위한 서킷 브레이커 도입을 검토 중입니다.
시스템의 복잡도를 제어하기 위해 이번 설계 단계에서 의도적으로 제외한 요소들이 있습니다.
화려한 어드민 UI 및 모니터링 대시보드: 초기 단계에 전용 UI를 구축하는 대신, 정형화된 구조의 애플리케이션 로그와 상태 테이블을 남기는 데 집중했습니다.
인프라가 투명하게 로그를 뱉어내면 대시보드가 없어도 문제를 추적할 수 있기 때문이라 생각이듭니다.
자체 Reranker 모델 파인튜닝: 검색 정확도를 무조건 끌어올리기 위한 모델 학습 대신, 텍스트가 유실 없이 완벽하게 잘 도달하는 인프라를 먼저 다졌습니다.
파이프라인이 부실하면 아무리 고도화된 Reranker를 붙여도 의미가 없다생각이 듭니다.
전수 OCR: 모든 문서를 무조건 OCR 처리하도록 설정하면 파이프라인의 연산 비용과 대기 시간이 과도하게 증가하는데 텍스트 추출이 기본적으로 가능한 문서를 빠르고 안정적으로 처리하는 구조를 정착시킨 뒤, 스캔 문서는 업로드 시 옵션에 따라 별도 워커가 가동되도록 비용을 격리했습니다.
안정적인 RAG 백엔드를 설계한다는 것은 최신 트렌드의 거대 모델을 연결하는 화려한 작업이 아니라 생각이듭니다.
데이터가 흘러가는 통로를 안전하게 확보하고, 예상치 못한 지점에서 병목이나 실패가 발생했을 때 정확히 어디를 열어보고 복구해야 하는지 시스템의 투명성을 확보하는 인프라를 구축하는 일이 이번 학습을 통한 배움이였습니다.