기술

Neo4j 그래프 탐색에서 삽질한 3가지 — null, Email 폭발, 자기참조

2026.04.086분 읽기

영향도 분석을 만들다가 쿼리에서 막혔다

지식그래프에 시각화 기능을 추가하고 있었거든요. "이 시스템을 건드리면 어디까지 영향이 퍼지나?"를 그래프 위에서 보여주는 영향도 분석 기능이었습니다. System 노드를 선택하면 1홉, 2홉, 3홉 거리까지 연결된 노드를 전부 찾아서 depth별로 색상을 다르게 표시하는 거예요.

개념은 단순했습니다. Neo4j에서 가변 길이 경로로 N홉까지 탐색하면 되니까요. 그런데 실제로 쿼리를 짜보니까 생각과 다른 문제가 세 가지 연달아 터졌습니다.


첫 번째 함정 — DEPENDS_ON만 탐색했더니 절반이 안 나온다

처음에는 DEPENDS_ON 관계만 가변 길이 경로로 탐색했습니다. 시스템 간 의존성을 따라가는 거니까 당연히 이 관계만 보면 된다고 생각했거든요.

MATCH path = (s:System {name: "물류서버"})<-[:DEPENDS_ON*1..3]-(dep)
RETURN path

결과를 보니까 연결된 시스템 1개만 나왔습니다. 그런데 해당 노드의 실제 이웃을 확인해보면 서비스, 미들웨어, 담당자까지 10개 이상이 연결돼 있었거든요.

원인은 단순했어요. RUNS_ON, MENTIONS_SYSTEM, MANAGES 같은 다른 관계 타입으로 연결된 노드들이 전부 빠진 거였습니다. 영향도 분석이라는 게 "이 시스템에 연결된 모든 것"을 봐야 하는데, DEPENDS_ON 하나만 보고 있으니 그래프의 절반을 놓치고 있었어요.


두 번째 함정 — 모든 관계를 탐색했더니 결과가 폭발했다

그래서 관계 타입을 지정하지 않고 모든 관계를 따라가는 쿼리로 바꿨습니다.

MATCH path = (s:System {name: "물류서버"})-[*1..3]-(connected)
RETURN path

이러니까 결과가 수만 건이 나왔습니다. 뭐가 문제인지 봤더니, Email 노드가 대량으로 끌려오고 있었어요. 시스템에 MENTIONS_SYSTEM 관계로 이메일이 연결돼 있는데, 이메일이 수천 건이거든요. 1홉에서 이메일 수천 건이 들어오고, 2홉에서 그 이메일에 연결된 사람·다른 시스템까지 끌려오니까 결과가 기하급수적으로 불어난 거예요.

영향도 분석에서 보고 싶은 건 "이 시스템과 연결된 시스템, 서비스, 담당자"인데, 이메일 수천 건이 결과에 섞이면 의미가 없습니다. 그래프 탐색의 방향은 맞는데, 필터링이 없으니까 관련 없는 노드까지 전부 따라간 거죠.

1.jpeg


세 번째 함정 — name이 null인 노드가 섞여 나온다

Email 폭발 문제를 해결하려고 Python에서 BFS를 직접 구현하고, 노드 타입별로 필터링을 넣었거든요. 그런데 결과를 보니까 이름 없는 노드가 섞여 있었습니다.

{"name": null, "type": "System", "depth": 2}

name이 null인 System 노드가 그래프에 존재하는 거였어요. LLM이 엔티티를 추출할 때 이름 없이 관계만 생성한 경우, 또는 임포트 과정에서 속성이 누락된 경우에 생기는 노드들이었습니다.

이게 영향도 분석 결과에 포함되면 "이름 없는 시스템이 영향받는다"고 표시되는데, 사용자 입장에서는 의미가 없거든요. 클릭해도 정보가 없으니까요.

해결은 BFS 탐색 과정에서 WHERE m.name IS NOT NULL 조건을 넣는 거였습니다. 단순하지만, 이걸 안 넣으면 결과에 쓰레기가 섞여서 신뢰도가 떨어져요.


Cypher 가변 길이 대신 Python BFS를 쓴 이유

세 가지 문제를 겪고 나니까, Cypher의 가변 길이 경로(*1..N)만으로는 한계가 있다는 게 분명해졌습니다. 결국 Python에서 BFS(너비 우선 탐색)를 직접 구현하는 방향으로 갔어요.

Cypher의 *1..N은 특정 관계 타입 하나에 대해서만 쓰기 좋습니다. 모든 관계 타입을 혼합 탐색하면서 depth별로 제어하려면 한계가 있거든요. 중간 노드의 depth 추적도 어렵고, Email 같은 대량 노드를 필터링할 수도 없어요.

Python BFS로 바꾸니까 세 가지 문제가 다 풀렸습니다. 각 홉에서 발견된 노드에 정확한 depth 값을 부여할 수 있고, visited 딕셔너리로 중복 방문을 차단하고, 노드 타입이나 name IS NOT NULL 같은 조건으로 유연하게 필터링이 됩니다.

frontier = [origin_id]
for current_depth in range(1, depth + 1):
    # frontier 노드들의 이웃을 Cypher로 조회
    # WHERE m.name IS NOT NULL 조건 포함
    # 이미 방문한 노드는 제외
    # 새로 발견된 노드를 다음 frontier로

Cypher가 탐색은 빠르지만, 복잡한 조건이 붙는 다단계 탐색에서는 애플리케이션 레벨에서 제어하는 게 낫더라고요.

2.jpeg


그래프 쿼리는 "쓰는 법"보다 "걸러내는 법"이 핵심입니다

정리하면, 세 가지 삽질 모두 "어떻게 탐색하느냐"가 아니라 "뭘 걸러내느냐"가 문제였습니다. 관계 타입을 하나만 보면 결과가 빠지고, 전부 보면 결과가 폭발하고, 필터 없이 보면 쓰레기가 섞여요.

Neo4j에서 N홉 탐색을 할 때 결과가 이상하다면, 쿼리 자체를 고치기 전에 "어떤 노드를 제외해야 하는지"부터 정하는 걸 추천합니다. 특히 Email처럼 관계가 대량으로 연결된 노드 타입이 있으면, 반드시 필터링 규칙을 먼저 잡아야 합니다.

#Neo4j#그래프쿼리#BFS#영향도분석#지식그래프#AI전환기