-
가벼운 그래프 데이터 처리 — NetworkX + SQLite WAL 조합의 정체와 효과IT 2026. 5. 27. 22:00
코드를 노드(함수·모듈·repo)와 화살표(호출·import·의존)로 표현하는 그래프 모델이 필요해진 순간, "전용 그래프 데이터베이스가 정답"이라는 첫 충동이 든다. Neo4j 같은 서버를 띄우거나, 임베디드 그래프 DB를 끼우거나. 매력적인 선택지들이다 — 전용 쿼리 언어, 토폴로지 알고리즘, 영속성 모두 갖춰져 있으니까.
그런데 막상 데이터 규모를 재 보면 의외의 결론에 닿는 경우가 많다. "수만 노드 규모면 더 가벼운 조합으로 충분하다"는 결론이다. 본인의 22개 repo를 인덱싱한 노드 수가 약 5,000개 수준이라면, 전용 그래프 DB는 과한 도구일 수 있다. 그 자리에 들어가는 더 작고 단순한 조합이 NetworkX + SQLite WAL이다.
이 글은 그 두 도구 — Python 그래프 라이브러리 NetworkX와 SQLite의 WAL 모드 — 가 각각 무엇이고, 왜 만들어졌고, 어떤 문제를 어떻게 푸는지, 그리고 둘을 묶었을 때 어떤 효과가 나는지를 풀어 본다.
1. NetworkX — Python으로 네트워크를 다루는 표준 도구
NetworkX는 2005년 미국 로스앨러모스 국립연구소(Los Alamos National Laboratory)의 Aric Hagberg, Pieter Swart, Dan Schult가 만든 "복잡한 네트워크를 Python으로 다루는 라이브러리"다. 네트워크라는 표현이 친숙하지 않다면 그래프라고 읽으면 된다 — 노드(점)와 그것들을 잇는 edge(선)의 집합.
탄생 배경은 학술 분야였다. 사회학자가 친구 관계 망을, 생물학자가 단백질 상호작용 망을, 정보학자가 인용 관계 망을 분석할 때 공통으로 "그래프 자료구조 + 표준 알고리즘"이 필요했다. 각자 따로 짜다 보니 결과 비교가 안 됐고, 공통 라이브러리가 절실했다. 그게 NetworkX의 출발점이다.
20년이 지난 지금, NetworkX는 Python 그래프 분석의 사실상 표준이 됐다. NumPy·SciPy·pandas 같은 데이터 분석 라이브러리와 같은 생태계 안에 있고, 거의 모든 Python 개발자가 한 번쯤 접해 본 도구다.
NetworkX가 풀어 주는 문제들
다이어그램 설명. NetworkX는 네 가지 카테고리에서 300개 가까운 알고리즘을 제공한다. 가장 자주 쓰는 자리는 구조 분석(circular import 같은 사이클 감지)과 중심성 분석(어느 모듈이 가장 많이 import되는 hub인가)이다. 같은 일을 직접 짜려면 BFS·DFS·메모리 관리를 다시 구현해야 하는데, NetworkX는 한 줄 호출로 끝난다.
NetworkX의 동작 방식 — Python dict 위의 그래프
내부 구현은 의외로 단순하다. Python의 dict(딕셔너리)를 중첩해서 그래프를 표현한다 — "노드 ID → 그 노드의 이웃들"이라는 매핑이 기본 자료구조다. 그래서 NumPy·SciPy 행렬을 쓰는 다른 그래프 라이브러리보다 메모리는 더 쓰지만, 임의의 Python 객체를 노드·edge 속성에 자유롭게 매달 수 있다는 큰 장점이 있다.
이 설계 덕분에 NetworkX는 "노드에 어떤 데이터든 갖다 붙일 수 있는 가볍고 친숙한 그래프"가 됐다. 단점은 명확하다 — 노드 수가 수백만을 넘어가면 메모리·속도 모두 한계가 보인다. 그래서 NetworkX 자체는 "수만~수십만 노드 규모"가 가장 자연스러운 자리다.
본인 입장에서 NetworkX의 효과
- 학습 비용 0에 가까움 — 새 쿼리 언어 안 익혀도 됨. Python을 아는 사람이면 30분 문서 읽기로 시작 가능.
- 전용 서버 0 — pip 한 줄로 설치, 별도 데몬·포트 없음. 내 프로그램 안에 라이브러리로 들어옴.
- 표준 도구라 어디서나 도움 받기 쉬움 — StackOverflow·GitHub issue·LLM 모두 NetworkX를 잘 안다. 문제 생기면 답이 빨리 나옴.
- NumPy·pandas·matplotlib과 자연스러운 시너지 — 그래프 분석 결과를 표·차트로 옮기는 다리가 짧다.
2. SQLite WAL — 가벼운 영속화의 동시성 비법
NetworkX는 그래프를 메모리 안에서만 다룬다. 프로그램이 종료되면 그래프도 사라진다. 그래서 디스크에 저장해 다음 실행에 다시 불러올 영속화(persistence) 메커니즘이 필요한데, 그 자리에 SQLite를 둔다.
SQLite는 "파일 한 개에 통째로 들어가는 임베디드 관계형 DB"다. 별도 서버 프로세스 없이 라이브러리로 호출하며, 한 번 들어 본 적 없는 사람이라도 안드로이드 폰·iOS 앱·웹 브라우저 안에 이미 들어가 있는 그 도구다. 세상에서 가장 많이 배포된 DB로 알려져 있다.
그런데 왜 굳이 "WAL 모드"인가
SQLite는 두 가지 저널링 모드를 지원한다 — 전통적인 rollback journal과 2010년에 도입된 WAL(Write-Ahead Logging)이다. 차이는 명확하다.
다이어그램 설명. rollback journal은 본 DB를 직접 수정하면서 백업 파일로 안전을 확보하는 방식이라 한 명이 쓰는 동안 다른 누구도 못 읽는다. WAL은 변경분을 별도 파일에 먼저 쌓고 본 DB는 그대로 두는 방식이라 쓰는 사람과 읽는 사람이 서로 막지 않는다. 백그라운드에서 그래프를 갱신하는 동안 다른 프로세스가 그래프를 읽어야 하는 자리에서 WAL의 가치가 크다.
본인 입장에서 WAL의 효과
- 인덱서가 그래프를 갱신하는 동안 viewer가 멈추지 않는다 — 야간 배치가 새 데이터를 쓰고 있어도, 대시보드는 같은 그래프를 안정적으로 조회한다.
- 락 관리 코드 작성 불필요 — DB 자체가 read·write 격리를 해 주므로, 응용 단에서 별도 락 변수를 만들 필요가 없다.
- commit이 빠름 — 변경분만 .wal에 fsync하면 끝. 본 DB 전체에 디스크 동기화를 안 함.
설정도 단순하다 — DB 연결 직후 한 줄이면 끝.
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") # 성능 ↑, 안전 살짝 ↓journal_mode=WAL이 모드를 켜고,synchronous=NORMAL은 fsync를 좀 덜 자주 해서 속도를 높인다(crash 시 마지막 트랜잭션 한두 개를 잃을 수 있지만, 인덱싱 같은 재실행 가능한 워크로드엔 안전 마진이 충분하다).3. 두 도구를 묶는 이유 — 메모리 속도 + 디스크 영구성
한 마디로 말하면 "각자가 잘하는 자리에 둔다"가 조합의 본질이다.
- NetworkX (메모리) — 그래프 알고리즘을 빠르게 돌리는 자리. dict 기반이라 노드·edge에 속성을 자유롭게 매단다. 단점은 프로세스 종료 시 사라짐.
- SQLite WAL (디스크) — 그래프 상태를 영속화하고, 동시 read를 허용하는 자리. 단점은 SQL이라 토폴로지 분석엔 어색함.
둘을 합치면 "디스크에서 상태를 안전하게 들고 있다가, 메모리에서 빠르게 분석한다"는 자연스러운 흐름이 완성된다. 프로세스 시작 시 SQLite에서 NetworkX로 로드하고, 갱신은 SQLite에 쓰고, 분석은 NetworkX 안에서 한다.
4. 코드로 보면 — 한 파일에 다 들어가는 graph store
실제 구현은
graph/store.py한 파일이다. 100줄도 안 된다.# graph/store.py — NetworkX in-memory + SQLite WAL 영속화 import sqlite3 from pathlib import Path import networkx as nx ROOT = Path.home() / "projects" / "deep-wiki" / "graph" SQLITE_PATH = ROOT / "state.sqlite" def _connect(path: Path = SQLITE_PATH) -> sqlite3.Connection: """SQLite 연결을 WAL 모드로 연다.""" path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(path)) # WAL이 핵심 — write가 진행 중이어도 read는 lock 없이 계속 가능 conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") # 스키마는 단순한 2-테이블 (nodes / edges) conn.execute(""" CREATE TABLE IF NOT EXISTS nodes( id TEXT PRIMARY KEY, kind TEXT NOT NULL, -- Repo/Module/Function/Symbol/... repo TEXT NOT NULL, commit_sha TEXT NOT NULL, lang TEXT, label TEXT, attrs_json TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS edges( src TEXT NOT NULL, dst TEXT NOT NULL, kind TEXT NOT NULL, -- CALLS/DEPENDS_ON/INVOKED_VIA/... commit_sha TEXT NOT NULL, attrs_json TEXT, PRIMARY KEY(src, dst, kind) ) """) return conn def load_graph() -> nx.MultiDiGraph: """SQLite → NetworkX. 매 프로세스 시작 시 1회 호출.""" g: nx.MultiDiGraph = nx.MultiDiGraph() if not SQLITE_PATH.exists(): return g conn = _connect() for row in conn.execute("SELECT id, kind, repo, ... FROM nodes"): g.add_node(row[0], kind=row[1], repo=row[2], ...) # edges도 같은 방식으로 add_edge return g코드 설명. 스키마는 nodes 테이블 + edges 테이블 두 개가 전부다. 노드 한 줄에 ID·종류·소속 repo·언어·라벨이 들어가고, edge 한 줄에 src·dst·관계 종류가 들어간다.
MultiDiGraph를 쓰는 이유는 같은 두 노드 사이에 종류가 다른 edge가 공존할 수 있기 때문 — 예를 들어 한 함수가 같은 모듈에 대해CALLS와DEPENDS_ON두 종류의 관계를 동시에 가질 수 있다.load_graph()는 프로세스 시작 직후 한 번만 호출되고, 그 다음부터는 메모리 안의g객체로 모든 분석이 이루어진다.5. 쿼리 패턴 — SQL이 80%, NetworkX가 20%
실전에서 이 조합으로 그래프 질문을 풀 때 자연스럽게 두 도구로 분담된다.
다이어그램 설명. 두 도구 사이에 임피던스 미스매치(impedance mismatch, 두 시스템의 데이터 모델이 달라 변환 코드가 많이 드는 현상)가 거의 없다. SQLite에서 로드해 NetworkX 객체로 만든 같은 그래프를 그대로 NetworkX 함수에 넘기면 된다. 데이터에 대한 질문은 SQL로, 그 데이터의 모양·구조에 대한 질문은 NetworkX로 — 자연스럽게 분담된다.
6. 운영하면서 체감한 효과
이 조합을 두고 일하면서 가장 만족스러운 부분은 디버깅 속도였다. SQLite는
sqlite3 graph/state.sqlite한 줄이면 즉시 CLI로 들어가 볼 수 있다.SELECT * FROM edges WHERE kind = 'INVOKED_VIA' LIMIT 10;한 줄로 끝나는 일이, 별도 그래프 DB라면 전용 CLI 익히기·전용 쿼리 짜기로 늘어난다. 디버깅 latency가 곧 작업 속도다.예상 못 한 부수 효과도 있었다. WAL이 read 동시성을 깔끔하게 해결해 줘서, 야간 배치가 그래프를 재인덱싱하는 동안에도 다른 viewer가 안정적으로 그래프를 읽을 수 있었다. 별도 락 관리 코드를 짤 필요가 없었다. 그리고 NetworkX는 mermaid·시각화 친화성 덕분에 자동 생성하는 의존성 다이어그램 페이지가 LLM 없이도 충분히 깔끔하게 떨어졌다.
남은 부담은 솔직히 적다. 매 프로세스 시작 시 SQLite → NetworkX 로드 시간이 있긴 하지만, 5천 노드 기준 1초 미만이다. 노드 수가 10만을 넘기 시작하면 그때 lazy load·indexed subgraph 같은 패턴을 도입하면 된다.
마무리 — 무거운 도구가 정답처럼 보일 때 한 번 의심해 보기
"그래프 데이터엔 그래프 DB"라는 직관은 강력하지만, 그 직관이 도구의 잠재 능력을 모두 활용한다고 보장하지는 않는다. 본인이 실제로 쓸 그래프 알고리즘이 BFS·중심성·사이클 감지 정도로 끝난다면, NetworkX가 그 자리를 충분히 채운다. 본인이 다룰 노드 수가 수만 규모라면, 별도 그래프 DB 서버 프로세스의 부담을 안 들여도 된다.
도구 선택의 정답이 절대적일 거라는 기대를 한 번 더 의심해 보면 좋겠다. "이 도구가 좋다"는 사실보다 "이 도구의 좋은 부분이 내 상황과 겹치는가"가 훨씬 중요한 질문이다. 그 질문에 정직하게 답할 때, 의외로 표준 라이브러리 + SQLite 조합이 가장 단순하면서도 강력한 선택이 된다. 가벼움은 종종 그 자체가 효과다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
NetworkX 대표 알고리즘 3선 — 코드 베이스 분석에서 한 줄로 끝나는 일들 (0) 2026.05.27 Cypher — SQL은 알지만 그래프 쿼리는 처음인 사람에게 (0) 2026.05.27 코드 위키는 mermaid를 얼마나 쓸까 — React 위키 115개·Express 위키 221개 실측과 의미 (0) 2026.05.26 코드 위키 8섹션 표준 — Overview부터 Glossary까지 하나씩 풀어보기 (0) 2026.05.26 DeepWiki·CodeWiki·deepwiki-open — 같아 보이는 코드 위키 도구 세 개의 진짜 차이 (0) 2026.05.26 한국어 자막 sync는 한 알고리즘으로 안 된다 — 4단 fallback을 쌓아 올린 이유 (0) 2026.05.25 Seedance 2.0 fast 영상의 음성이 1.3초 빨리 나왔다 — 재생성 0원, ffmpeg adelay로 5초만에 해결 (0) 2026.05.24 ffmpeg concat이 Seedance 클립 두 번째부터 깨졌다 — video duration이 진실의 원천 (0) 2026.05.24 ffmpeg -c copy로 0.5초 trim했더니 0초 trim됐다 — keyframe-aligned의 함정 (Seedance 후처리 사례) (0) 2026.05.24 Seedance 2.0 fast 영상의 첫 0.5초가 사진처럼 정지된 이유 — photo prefix 자동 제거 (0) 2026.05.23