카테고리 없음

Langflow + Neo4j Community Edition 으로 폐쇄망 GraphRAG 만들기 (Aura에서 Docker 자체호스팅 전환 후기)

임마누엘한 2026. 5. 14. 15:42

들어가며

전날 만든 Langflow + 한국어 임베딩 위에, 이번에는 Neo4j 그래프 DB 를 붙여서 GraphRAG (Graph Retrieval-Augmented Generation) 데모를 만들기로 했다. GraphRAG 의 핵심은 "벡터 유사도만으로는 못 잡는 관계 정보 (entity-relationship) 를 그래프 쿼리로 보강" 하는 것. 예를 들어 "이 고객이 산 상품의 다른 구매자는 누구?" 같은 질문은 벡터 검색만으로는 막막하지만 Cypher 한 줄이면 된다.

처음에는 빠르게 시작하기 위해 Neo4j Aura (managed cloud) 로 PoC 를 만들었는데, 운영 요구사항이 "폐쇄망 환경" 으로 바뀌면서 Community Edition 을 Docker 로 자체호스팅 하는 쪽으로 갈아탔다. 그 전환 과정에서 겪은 함정과, 자연어→Cypher 매핑의 한계, Langflow 사이드바에 커스텀 컴포넌트 영구 등록하는 방법까지 한 번에 정리한다.

핵심 키워드: Neo4j Community Edition, Docker, GraphRAG, Langflow custom tool, Cypher, APOC, LANGFLOW_COMPONENTS_PATH.

시스템 컨텍스트

  • 데이터 모델: 고객(Customer) → 주문(Order) → 상품(Product). 관계는 PURCHASED, CONTAINS.
  • 데모 시나리오: 비즈니스 사용자가 자연어로 질문 → Langflow agent → tool 선택 → Neo4j 조회 → 답변 합성.
  • 운영 환경: 처음엔 macOS 로컬, 최종 목표는 on-prem 폐쇄망.
사용자 ─NL─> Langflow agent ─tool 선택─> Neo4j Cypher 도구 ─bolt─> Neo4j
                              └─────tool 선택─> 벡터 검색 도구 ─────> Chroma

문제 상황

첫 두 질문으로 데모 가능성을 가늠.

  • Q1: "주문횟수가 5회 이하인 고객을 찾아줘" → ✅ 잘 동작 (단순 필터)
  • Q2: "가장 주문수가 많은 고객은?" → ❌ Langflow agent 가 거절:

"전체 데이터 대상 랭킹/정렬 쿼리는 현재 도구로 지원하지 않습니다."

또한 운영 요구사항 변경:

  • "이 PoC 는 곧 사내 폐쇄망에 올라가야 한다."
  • → Aura (외부 클라우드) 사용 불가능. 자체 호스팅 가능한 옵션이 필요.

세 번째 문제는 의외의 함정:

  • 캔버스에 한 번 만든 커스텀 Neo4j tool 이 다른 flow 에서는 사이드바에 안 보임.

시도한 것들

시도 1 — Aura Free 로 빠르게 PoC

처음엔 Aura Free 에 데이터를 올려서 테스트했다. 가입 → 인스턴스 생성 → 데이터 로드까지 10분 안에 가능. 빠른 PoC 에는 좋다. 그러나 두 가지 한계가 빠르게 드러났다.

Aura 의 패스워드 정책.

"Aura 비밀번호가 뭐 말하는거?"

라는 의문이 한 번 들었던 게, Aura 는 인스턴스 생성 시 한 번 보여주는 generated password 를 사용자가 캡쳐해두지 않으면 reset 외에는 방법이 없다. 운영 환경에서는 시크릿 관리 정책과 안 맞을 가능성. PoC 단계에서 비밀번호 분실로 인스턴스 reset 한 번 했다.

폐쇄망 요건과 충돌.

운영 요구사항이 바뀌면서 Aura 위에 만든 모든 PoC 자산을 마이그레이션 해야 했다. 데이터 dump 후 self-host 로 복원하는 건 가능하지만, 처음부터 self-host 로 갔으면 추가 작업이 없었을 영역. 결국 PoC 단계부터 deployment target 을 명확히 하는 게 시간 절약이라는 교훈.

시도 2 — Community Edition Docker 셋업

가장 단순한 단일 노드 셋업.

docker run -d \
  --name neo4j-community \
  -p 7474:7474 \
  -p 7687:7687 \
  -e NEO4J_AUTH=neo4j/changeme \
  -e NEO4J_PLUGINS='["apoc"]' \
  -v $HOME/neo4j/data:/data \
  -v $HOME/neo4j/logs:/logs \
  --restart unless-stopped \
  neo4j:5-community

각 옵션의 의미:

  • -p 7474:7474 → HTTP 콘솔 (브라우저 UI).
  • -p 7687:7687 → Bolt 드라이버 포트 (Python neo4j driver 가 붙는 곳).
  • NEO4J_AUTH=neo4j/changeme → 초기 비밀번호. user/password 한 줄 포맷. 실서비스에서는 반드시 강한 패스워드로 변경.
  • NEO4J_PLUGINS='["apoc"]' → APOC 플러그인 자동 설치. 거의 모든 advanced 쿼리에서 필요.
  • 볼륨 두 개 → 컨테이너 재기동 시 데이터·로그 유지.

검증:

docker logs neo4j-community | tail
# "Started." 가 보이면 OK

브라우저에서 http://localhost:7474 접속 → neo4j / changeme 로그인 → 초기 비밀번호 변경 강제됨.

시도 3 — Community Edition 의 한계 인지

Community 로 갈아탔더니 사용 가능한 기능이 줄어든다. 알고 가야 할 차이:

기능CommunityEnterprise / Aura Pro

다중 사용자 (RBAC) ❌ 단일 사용자
Multi-database ❌ 1개 DB
Role-based access
Causal Clustering (HA)
Hot backup
Property-level security

폐쇄망 운영에서 multi-tenant RBAC 가 필요해진 시점에 우리는 애플리케이션 레벨 RBAC 컴포넌트를 별도로 만들어서 우회했다. Cypher 쿼리에 자동으로 WHERE tenant_id = $tenant 같은 절을 강제 주입하는 작은 wrapper. Enterprise 가 깔끔하지만 라이선스 비용 대비 PoC 단계에서는 과한 선택이었다.

시도 4 — 자연어 → Cypher 의 한계

Langflow 의 default tool 만으로는 ranking, aggregation 류 쿼리가 약했다. 예를 들어 "가장 주문 많은 고객" 같은 질문은 다음 Cypher 로 끝나는데:

MATCH (c:Customer)-[:PURCHASED]->(o:Order)
RETURN c.name AS name, count(o) AS orders
ORDER BY orders DESC
LIMIT 5

agent 가 "ranking 쿼리는 지원 안 됨" 으로 거절한다. 이유: agent 가 가진 tool 의 description 이 "특정 키워드 기반 검색과 관계 탐색만 지원" 이라고 적혀 있어서, ranking 의도가 매칭이 안 됐다.

해결: 시나리오별 custom tool 을 명시적으로 추가. 자연어 의도 → tool 매핑이 정확해진다.

from langflow.custom import Component
from langflow.io import StrInput, IntInput, Output
from neo4j import GraphDatabase

class Neo4jTopCustomers(Component):
    display_name = "Top Customers by Orders"
    description = "주문 수가 많은 상위 N명의 고객을 반환한다. " \
                  "'가장 주문 많은 고객', 'top buyers', '주문 랭킹' 류 질문에 적합."

    inputs = [
        StrInput(name="uri", display_name="Bolt URI",
                 value="bolt://localhost:7687"),
        StrInput(name="user", display_name="User", value="neo4j"),
        StrInput(name="password", display_name="Password", value=""),
        IntInput(name="top_n", display_name="Top N", value=5),
    ]

    outputs = [
        Output(display_name="Result", name="result",
               method="run", types=["Data"])
    ]

    def run(self):
        with GraphDatabase.driver(
            self.uri, auth=(self.user, self.password)
        ) as driver, driver.session() as sess:
            rows = sess.run(
                """
                MATCH (c:Customer)-[:PURCHASED]->(o:Order)
                RETURN c.name AS name, count(o) AS orders
                ORDER BY orders DESC
                LIMIT $n
                """,
                n=self.top_n,
            )
            return [r.data() for r in rows]

핵심은 description 에 자연어 패턴을 충분히 적는 것. agent 가 이 description 을 보고 tool 호출 여부를 결정하므로, "랭킹", "top", "가장 많은", "정렬" 같은 후보 키워드를 description 에 흩뿌려 둔다.

시도 5 — 사이드바에 영구 등록

캔버스에서 만든 컴포넌트는 그 flow 안에만 존재한다. 다른 flow 에서도 같은 tool 을 쓰려면 LANGFLOW_COMPONENTS_PATH 환경변수로 등록.

export LANGFLOW_COMPONENTS_PATH=$HOME/langflow-components
mkdir -p $HOME/langflow-components/neo4j_tools
# 그 안에 .py 파일들 저장

디렉토리 구조 규칙:

$LANGFLOW_COMPONENTS_PATH/
├── neo4j_tools/                    ← 카테고리 폴더
│   ├── neo4j_top_customers.py      ← 컴포넌트 파일
│   ├── neo4j_customer_search.py
│   └── neo4j_relationship.py
└── embeddings_korean/
    └── ko_embeddings.py

폴더 이름이 사이드바의 카테고리. __init__.py 는 굳이 필요 없다. 등록 후 Langflow 재시작.

시도 6 — Bolt 연결 확인 (Langflow 안 거치고)

Langflow tool 이 안 도는 게 코드 문제인지 Neo4j 자체 문제인지 분리하기 위해, 항상 터미널에서 driver 로 직접 한 번 쳐본다.

# verify_neo4j.py
from neo4j import GraphDatabase

with GraphDatabase.driver("bolt://localhost:7687",
                           auth=("neo4j", "your-password")) as driver:
    with driver.session() as sess:
        result = sess.run("MATCH (n) RETURN count(n) AS total")
        print(result.single()["total"])

python verify_neo4j.py 가 도는 순간 DB 자체는 문제 없음 확정. 그러면 그 다음 분리는 Langflow tool code → agent prompt 순서로 좁힌다.

시도 7 — Aura → Self-host 데이터 마이그레이션

Aura 의 admin 페이지에서 dump 파일을 받고, self-host Neo4j 의 import/ 디렉토리에 넣고 복원.

docker exec -it neo4j-community \
  neo4j-admin database load neo4j --from-path=/import --overwrite-destination=true

복원 후 데이터 개수 검증을 잊지 말 것 (MATCH (n) RETURN count(n) 을 Aura 측·self-host 측 둘 다에서 돌려 비교).

해결 — 최종 구성

사용자 NL
   │
   ▼
Langflow agent ───┬──> Ko-SRoberta 임베딩 (전날 만든 컴포넌트)
                  │           │
                  │           ▼
                  │       Chroma (벡터 검색)
                  │
                  ├──> Neo4j Customer Search   ─┐
                  ├──> Neo4j Relationship Explore ──> Neo4j 5-community (Docker)
                  └──> Neo4j Top Customers     ─┘
  • Neo4j Community Edition (Docker) on-prem.
  • Langflow custom tools 4종 (검색 / 관계 탐색 / ranking / 한국어 임베딩) 을 LANGFLOW_COMPONENTS_PATH 로 영구 등록.
  • agent prompt 에 "어떤 질문에서 어떤 tool 을 쓰는지" 가이드라인 명시. 자연어 매핑 정확도가 크게 상승.

데모 결과: 처음 거절당했던 "가장 주문 많은 고객" 질문이 잘 응답. 자연어 → Cypher 매핑이 시나리오별 tool 분리로 안정화.

검증 절차

  1. docker ps 로 neo4j-community 가 떠 있는지.
  2. http://localhost:7474 브라우저 콘솔 로그인.
  3. python verify_neo4j.py 로 driver 연결 확인.
  4. Langflow 캔버스 사이드바 → "neo4j_tools" 카테고리 4개 보이는지.
  5. 데모 질문 5개 (단순 검색·관계 탐색·집계·ranking·복합) 차례로 던져서 정상 응답 확인.

배운 점

  • Deployment target 을 PoC 단계부터 정확히 잡자. Aura → self-host 마이그레이션은 가능하지만, RBAC 모델 / 패스워드 정책 / 데이터 dump 모두 추가 작업이 붙는다. "PoC 는 Aura, 운영은 self-host" 라는 단순한 결정이 두 번 일하게 만든다.
  • Langflow agent 의 tool description 이 의도 매칭의 거의 전부. "랭킹", "top", "정렬", "집계", "최대" 같은 한국어 키워드를 description 에 자연스럽게 흩뿌려 두면 매핑 정확도가 크게 오른다. 영어로만 적으면 한국어 질의에서 매칭이 약해진다.
  • LANGFLOW_COMPONENTS_PATH 는 RAG 데모 반복 시 필수. 캔버스 내 등록은 휘발성에 가깝다. 폴더 = 카테고리, 파일 = 컴포넌트의 규칙을 처음부터 따르자.
  • Neo4j Community + APOC 조합은 PoC 까지는 거의 모든 시나리오 커버. Multi-DB / RBAC / Causal Clustering 이 필요해지는 순간이 Enterprise 라이선스 검토 시점.
  • 자연어 → DB 매핑 디버깅은 항상 driver 로 직접 한 번 쳐본 뒤 시작하자. DB 문제 / tool code 문제 / agent prompt 문제를 분리해야 30분 안에 원인을 좁힐 수 있다.

다음 단계

  • Custom tool 에 자동 tenant_id 주입 wrapper 적용해서 application-level RBAC 강화.
  • Cypher 쿼리에 LIMIT 자동 강제로 cost 폭발 방지.
  • agent 가 어떤 tool 을 골랐는지 trace 를 별도 로그로 저장 → 의도 매칭 실패 케이스 회수.

참고