RAG 시스템을 처음 만들 때 나는 LLM 선택과 프롬프트 튜닝에 거의 모든 시간을 쏟았다. 어떤 임베딩 모델이 좋은지, 청크 크기를 얼마로 할지, 리랭커를 붙일지 말지. 그런데 막상 프로덕션에 올리고 나니 진짜 골치 아픈 건 전혀 다른 데 있었다.
“어제 인사팀이 게시판에 올린 새 규정, 챗봇이 왜 모르지?”
이 한마디가 모든 걸 말해준다. RAG의 답변 품질은 결국 지식베이스가 얼마나 최신이고 정확하냐에 달려 있는데, 문서가 여기저기 흩어져 있고, 매번 수동으로 올려야 하고, 새 문서가 추가돼도 자동으로 반영이 안 된다면 그 RAG는 출시한 순간부터 낡기 시작한다.
이걸 해결하려고 한참 헤매다 도착한 곳이 Apache NiFi다. 데이터 엔지니어링 쪽에서는 오래된 도구인데, RAG 전처리 파이프라인 관점에서 다시 보니 이만한 물건이 없었다. 이 글은 NiFi를 RAG 데이터 파이프라인의 수집·전처리 레이어로 쓰는 방법을, 내가 실제로 구성하면서 배운 것 중심으로 풀어보는 글이다.
Apache NiFi, 일단 이름부터
사소하지만 짚고 가자. Apache NiFi는 “나이파이(NI-FY)”로 읽는다. “니피”나 “니어파이”가 아니다. 이름의 유래는 NiagaraFiles로, 원래 미국 NSA에서 개발해 오픈소스로 공개한 데이터 플로우 관리 도구다.
NiFi를 한 문장으로 설명하면 “데이터를 수집하고, 변환하고, 라우팅하는 흐름을 코드 없이 시각적으로 설계하는 도구”다. 웹 UI 캔버스에서 프로세서(Processor)라는 블록들을 드래그해서 배치하고, 선으로 연결해서 데이터가 흘러가는 길을 만든다. 흐름 기반 프로그래밍(Flow-Based Programming) 개념에 뿌리를 두고 있다.
여기서 RAG와 연결되는 핵심이 있다. RAG의 문서 수집과 전처리는 본질적으로 “여러 소스에서 문서를 가져와서, 종류별로 분류하고, 파싱하고, 적절한 곳으로 보내는” 데이터 플로우다. NiFi가 바로 이걸 위해 태어난 도구다.
왜 RAG 전처리에 NiFi인가
RAG 전처리를 Python 스크립트로 짤 수도 있다. 실제로 많이들 그렇게 한다. 그런데 규모가 커지면 스크립트 방식은 금방 한계에 부딪힌다.
문서 소스가 하나가 아니다. 사내 NAS 폴더, 그룹웨어 게시판, 이메일 첨부파일, 데이터베이스 레코드, 외부 API. 이 각각을 감시하고 가져오는 코드를 따로 짜고, 스케줄러를 붙이고, 실패하면 재시도하는 로직을 만들고, 어떤 문서가 언제 처리됐는지 추적하는 것까지. 이걸 다 직접 구현하면 RAG 본체보다 파이프라인 코드가 더 커진다.
NiFi는 이 모든 걸 기본으로 제공한다. 특히 RAG 파이프라인에서 빛나는 NiFi의 강점이 몇 가지 있다.
첫째, 데이터 프로버넌스(Data Provenance)다. NiFi는 모든 데이터 조각이 언제 들어와서, 어떤 프로세서를 거쳐, 어디로 갔는지를 전부 추적한다. “이 청크가 원래 어느 문서의 몇 페이지에서 왔지?”를 역추적할 수 있다는 건 RAG 디버깅에서 엄청난 자산이다. 답변이 이상할 때 출처를 끝까지 따라갈 수 있다.
둘째, 백프레셔(Back Pressure)와 큐 관리다. 문서가 한꺼번에 수천 개 쏟아져 들어와도, NiFi가 큐에 버퍼링하면서 다운스트림(임베딩 서버 등)이 감당할 수 있는 속도로 흘려보낸다. 임베딩 API에 초당 수백 건씩 때려넣어서 터지는 사고를 구조적으로 막아준다.
셋째, NiFi 2.x에서 Python 프로세서를 정식 지원하기 시작했다. Java 21 기반으로 완전히 새로 쓰여진 NiFi 2는 CPython을 지원하면서 pandas, scikit-learn 같은 라이브러리를 흐름 안에서 직접 쓸 수 있게 됐다. 별도 저장소에서 벡터 DB 연동, 문서 청킹, OpenAI 연동을 위한 RAG용 예제 프로세서까지 제공한다. RAG 파이프라인을 NiFi 안에서 더 많이 처리할 수 있게 됐다는 뜻이다.
폴링이냐 이벤트냐 – NiFi의 데이터 수집 방식
NiFi를 RAG에 쓰려면 데이터를 어떻게 가져올지부터 정해야 한다. NiFi는 두 가지 방식을 다 지원하는데, 기본은 폴링 방식이다.
폴링 기반은 NiFi가 주기적으로 소스를 확인해서 새 데이터를 가져오는 방식이다. GetFile, GetFTP 같은 프로세서가 정해진 스케줄(timer 또는 cron)에 따라 폴더를 들여다보고 새 파일을 집어온다. RAG 문서 수집에서 가장 많이 쓰는 방식이다. 사내 문서는 실시간으로 1초 단위까지 반영할 필요가 없는 경우가 대부분이라, 몇 분 간격 폴링이면 충분하다.
이벤트/푸시 기반도 지원한다. ListenHTTP는 HTTP POST를 수신해서 웹훅처럼 동작하고, ListenTCP, ListenSyslog는 소켓이나 syslog를 수신한다. ConsumeKafka로 Kafka 이벤트를 소비할 수도 있다. 외부 시스템이 “새 문서 생겼어”라고 NiFi에 직접 알려주는 구조가 필요하면 이쪽을 쓴다.
한 가지 알아둘 점은 SSE나 WebSocket은 NiFi에 기본 내장돼 있지 않다는 거다. 필요하면 커스텀 프로세서를 개발하거나, 앞단에 별도 서버를 두고 NiFi는 HTTP나 Kafka로 받는 구조로 우회해야 한다. RAG 전처리에서는 SSE/WS가 거의 필요 없으니 크게 신경 쓸 일은 아니다.
실전 아키텍처 – 문서를 바이너리 스트림으로 빨아들이기
이제 실제 구조를 보자. 내가 구성한 RAG 전처리 파이프라인의 전체 그림은 이렇다.
문서 소스(폴더/NAS/이메일, 공문서/게시판, DB)가 NiFi 레이어로 들어오고, NiFi가 파일 타입을 라우팅해서 RAG 백엔드(FastAPI + 파싱 + 청킹/임베딩 + pgvector)로 넘긴다. 그리고 최종적으로 LangGraph 기반 RAG 에이전트가 그 벡터 저장소를 검색해서 사용자에게 답한다.
NiFi 레이어 내부를 좀 더 자세히 보면 핵심 흐름은 이렇게 짜여진다.
NAS를 마운트한 뒤 ListFile 프로세서로 폴더를 감시한다. ListFile은 폴더 안의 파일 목록과 메타데이터(파일명, 크기, 수정 시각)만 가져오고 실제 내용은 안 읽는다. 그리고 상태를 기억하기 때문에, 이미 처리한 파일은 건너뛰고 새로 추가되거나 변경된 파일만 잡아낸다. 이게 RAG에서 중요한 이유는, 매번 전체 폴더를 다시 처리하지 않고 변경분만 증분 처리할 수 있기 때문이다.
다음으로 FetchFile 프로세서가 실제 바이너리를 읽는다. ListFile이 “이 파일이 새로 생겼어”라고 알려주면, FetchFile이 그 파일의 실제 내용을 FlowFile의 콘텐츠로 읽어들인다. 목록 조회와 실제 읽기를 분리한 이 패턴이 NiFi의 전형적인 설계인데, 분산 환경에서 특히 유용하다. 목록은 한 노드가 잡고, 실제 무거운 읽기는 여러 노드가 나눠서 처리할 수 있다.
그다음이 핵심인 RouteOnAttribute다. 파일 확장자나 MIME 타입을 기준으로 흐름을 분기한다. PDF는 PDF 파싱 경로로, HWP는 HWP 경로로, DOCX는 DOCX 경로로 갈라진다. 한국 공공기관이나 기업 문서를 다뤄본 사람은 알겠지만, HWP 처리가 항상 골칫거리다. 이렇게 타입별로 라우팅해두면 각 포맷에 맞는 전용 파서로 보낼 수 있다.
파싱은 NiFi 밖으로 – InvokeHTTP로 FastAPI 호출
여기서 설계 판단이 하나 갈린다. 문서 파싱을 NiFi 안에서 할 것인가, 밖에서 할 것인가.
내 선택은 밖이다. 복잡한 문서 파싱, 특히 PDF의 레이아웃 분석이나 표 추출, OCR 같은 작업은 전용 라이브러리가 훨씬 잘한다. 그래서 NiFi는 라우팅까지만 하고, 실제 파싱은 별도 FastAPI 서버에 맡긴다.
RouteOnAttribute에서 갈라진 각 경로는 InvokeHTTP 프로세서로 연결된다. InvokeHTTP가 파일 바이너리를 HTTP POST로 FastAPI 엔드포인트에 보낸다. 이때 content-type을 application/octet-stream으로 설정해서 바이너리 스트림 그대로 전송한다.
PDF ──→ InvokeHTTP POST /etl/pdf (application/octet-stream)
HWP ──→ InvokeHTTP POST /etl/hwp
DOCX ──→ InvokeHTTP POST /etl/docx
FastAPI 쪽에서는 각 엔드포인트가 해당 포맷 전용 파서를 돌린다. PDF라면 MinerU 같은 도구로 레이아웃을 분석하고 텍스트와 표를 추출한다. 이렇게 파싱 로직을 NiFi 밖 파이썬 서비스로 빼면, 파서를 독립적으로 업그레이드하거나 GPU 서버에 배치하는 것도 자유로워진다.
이 분리가 주는 이점이 크다. NiFi는 “언제, 무엇을, 어디로 보낼지”라는 오케스트레이션에 집중하고, 무거운 연산은 전문 서비스가 담당한다. 각 레이어를 독립적으로 확장하고 교체할 수 있다. MinerU를 다른 파서로 바꿔도 NiFi 흐름은 그대로다.
DB 변경 감지 – QueryDatabaseTable
문서 소스가 파일만 있는 게 아니다. 사내 시스템의 데이터베이스에 쌓이는 게시글, 공지사항, 상담 기록도 RAG의 중요한 지식 소스다.
NiFi의 QueryDatabaseTable 프로세서가 이걸 처리한다. 이 프로세서는 테이블을 주기적으로 조회하되, 특정 컬럼(보통 타임스탬프나 증분 ID)을 기준으로 마지막에 처리한 지점을 기억한다. 그래서 다음 실행 때는 그 이후에 추가되거나 변경된 레코드만 가져온다. 전체 테이블을 매번 다시 읽지 않고 변경분만 증분 수집하는 거다.
예를 들어 게시판 테이블에서 updated_at 컬럼을 기준 컬럼으로 지정하면, NiFi가 마지막 처리 시각 이후에 수정된 글만 가져온다. 새 공지가 올라오거나 기존 글이 수정되면 자동으로 RAG 파이프라인에 흘러 들어간다. 인사팀이 게시판에 새 규정을 올리면 몇 분 안에 챗봇이 알게 되는 구조가 이렇게 완성된다.
청킹과 임베딩, 그리고 pgvector
파싱이 끝난 텍스트는 청킹과 임베딩을 거쳐 벡터 저장소에 들어간다. 이 부분도 FastAPI 백엔드에서 처리하는 경우가 많다.
파싱된 텍스트를 의미 단위로 잘라 청크를 만들고, 임베딩 모델로 각 청크를 벡터로 변환한 뒤, pgvector에 저장한다. pgvector는 PostgreSQL의 확장으로, 기존 RDB에 벡터 검색 기능을 더한 것이다. 별도 벡터 DB를 운영하지 않고 이미 쓰던 PostgreSQL에 벡터를 같이 저장할 수 있어서, 인프라가 단순해지는 게 장점이다. 메타데이터(출처 문서, 페이지, 작성일)와 벡터를 같은 테이블에서 관리할 수 있다는 것도 RAG에서 유용하다.
이 단계까지 NiFi 안에서 처리하고 싶다면, NiFi 2.x의 Python 프로세서나 RAG용 예제 프로세서를 활용할 수도 있다. 다만 나는 청킹·임베딩 전략을 자주 바꾸는 편이라, 이 부분은 FastAPI 코드로 관리하면서 빠르게 실험하는 쪽을 선호한다. 정답이 있는 건 아니고 팀의 운영 방식에 따라 선택하면 된다.
LangGraph RAG 에이전트로 연결
이렇게 pgvector에 벡터가 쌓이면, 그다음은 검색과 생성이다. 여기서 LangGraph 기반 RAG 에이전트가 등장한다.
사용자 질문이 들어오면 에이전트가 pgvector에서 관련 청크를 검색하고, 필요하면 질문을 재구성하거나 여러 번 검색하고, 검색 결과를 바탕으로 LLM이 답변을 생성한다. 단순 “검색 후 생성”을 넘어, 검색 결과가 부족하면 다시 검색하거나, 질문을 분해해서 단계적으로 처리하는 식의 정교한 흐름을 LangGraph로 설계할 수 있다.
여기서 NiFi가 만들어둔 데이터 프로버넌스가 다시 빛난다. 에이전트가 특정 청크를 근거로 답변했을 때, 그 청크가 어느 문서에서 왔는지 끝까지 추적할 수 있다. “이 답변의 출처는 인사규정 3장 12조”라고 정확히 짚어줄 수 있는 건, 수집 단계부터 메타데이터를 일관되게 따라왔기 때문이다.
전체 그림을 다시 정리하면 이렇다. NiFi가 NAS·게시판·DB에서 문서를 자동으로 빨아들이고, 타입별로 분류해서 파싱 서버로 보내고, 결과가 청킹·임베딩을 거쳐 pgvector에 쌓이고, LangGraph 에이전트가 그걸 검색해 답한다. 사람이 손댈 일은 거의 없다.
WSL2 + Docker로 NiFi 띄우기
직접 해보려는 분들을 위해 환경 구성도 짚어두겠다. 개발 단계에서는 WSL2에 Docker로 NiFi를 띄우는 게 가장 편하다.
docker run --name nifi \
-p 8443:8443 \
-e SINGLE_USER_CREDENTIALS_USERNAME=admin \
-e SINGLE_USER_CREDENTIALS_PASSWORD=adminpassword123 \
apache/nifi:latest
이렇게 띄우면 Windows 브라우저에서 https://localhost:8443/nifi로 바로 접근된다. WSL2가 포트포워딩을 자동으로 해주기 때문에 별도 설정이 필요 없다.
주의할 점은 메모리다. NiFi는 최소 2~4GB는 잡아야 안정적으로 돈다. WSL2를 쓴다면 .wslconfig에서 메모리 제한을 확인해두는 게 좋다. 요즘 나오는 노트북이면 메모리 여유가 있어서 큰 문제는 없을 거다.
운영 환경으로 가면 NiFi 2의 Git 기반 Flow Registry를 활용하는 걸 추천한다. 흐름 정의를 Git 저장소에 버전 관리하면서 CI/CD로 배포할 수 있다. “Everything as Code” 원칙에 맞게 파이프라인을 코드처럼 관리할 수 있다는 뜻이다.
NiFi가 만능은 아니다
균형을 위해 한계도 짚자.
NiFi는 학습 곡선이 제법 있다. 프로세서 종류가 워낙 많고, FlowFile, 백프레셔, 스케줄링 같은 개념을 이해해야 제대로 쓸 수 있다. 단순히 “폴더 하나 감시해서 임베딩하는” 수준이라면 NiFi는 과한 도구일 수 있다. 그 정도는 Python 스크립트에 cron 하나로 충분하다.
NiFi가 빛나는 건 소스가 여럿이고, 포맷이 다양하고, 증분 처리와 추적이 중요하고, 안정적인 운영이 필요한 규모에서다. 사내 여러 부서의 문서를 통합하는 전사 RAG, 공공기관의 대규모 문서 수집, 지속적으로 갱신되는 지식베이스 같은 경우다. 반대로 소규모 프로토타입이라면 더 가벼운 도구로 시작하는 게 맞다.
또 하나, 무거운 파싱이나 ML 연산을 NiFi 안에서 다 하려고 하면 NiFi가 병목이 된다. 앞서 말했듯 무거운 작업은 외부 서비스로 빼고, NiFi는 오케스트레이션에 집중시키는 설계가 안정적이다.
RAG의 절반은 데이터 파이프라인이다
다시 처음으로 돌아가자. RAG를 만들 때 우리는 LLM과 프롬프트에 집중하기 쉽지만, 실제 운영에서 답변 품질을 결정하는 건 지식베이스가 얼마나 최신이고 정확하게 유지되느냐다. 그리고 그걸 책임지는 게 데이터 파이프라인이다.
Apache NiFi는 이 전처리 레이어를 코드 없이, 안정적으로, 추적 가능하게 만들어주는 도구다. ListFile과 FetchFile로 NAS를 감시하고, RouteOnAttribute로 포맷을 분류하고, InvokeHTTP로 파싱 서버를 호출하고, QueryDatabaseTable로 DB 변경을 감지한다. 무거운 파싱과 임베딩은 FastAPI 백엔드에 맡기고, 결과는 pgvector에 쌓이고, LangGraph 에이전트가 그걸 검색해 답한다. 각 레이어가 명확히 분리돼서 독립적으로 확장하고 교체할 수 있다.
NiFi 2가 Python을 정식 지원하고 RAG용 예제 프로세서까지 제공하기 시작하면서, 이 흐름은 앞으로 더 매끄러워질 거다. 데이터 엔지니어링 도구와 AI 파이프라인의 경계가 점점 흐려지고 있다.
RAG를 제대로 운영하고 싶다면, LLM을 바꾸기 전에 데이터가 어떻게 흘러들어오는지부터 들여다보자. 진짜 병목은 거기 있을 가능성이 높다.