- 그래프는 의존성, 웹 링크, 모델 체커의 상태 공간, 관계형 DB의 외래 키처럼 소프트웨어 곳곳에 있지만, 주류 프로그래밍 언어에는 내장 타입이나 표준 라이브러리 지원이 거의 없음
- 내장 그래프 타입을 만들기 어려운 첫 이유는 방향·무방향, 단순·멀티, 하이퍼그래프처럼 그래프 종류가 많고, 특정 성질이 알고리듬 선택과 성능을 크게 바꾸기 때문임
- 엣지 리스트, 인접 리스트, 인접 행렬, 참조 구조체 등 표현 방식마다 메모리와 조회 성능이 달라 하나의 범용 표현으로 모든 사용 사례를 만족시키기 어려움
- 그래프 알고리듬은 구현이 어렵고 큰 입력에서 실행되는 경우가 많아, Nosey Parker와 Gecode 사례처럼 문제 맞춤형 표현과 순회가 범용 라이브러리보다 중요해질 수 있음
- 표준 라이브러리에 그래프가 드문 이유는 타입·표현·알고리듬·성능 트레이드오프와 유지보수 부담이 크기 때문이며, 서드파티 라이브러리도 제한적이거나 느릴 수 있음
그래프는 흔하지만 언어 지원은 부족함
- 그래프는 노드와 엣지로 구성되며, 노드와 엣지에는 데이터가 들어갈 수 있음
- 소프트웨어 엔지니어링에서 그래프는 여러 형태로 등장함
- 패키지 의존성과 모듈 import는 방향 그래프를 이룸
- 인터넷은 웹페이지 사이 링크 그래프임
- 모델 체커는 가능한 모든 설정의 상태 공간을 탐색하며, 노드는 상태이고 엣지는 유효한 전이임
- 관계형 데이터베이스는 레코드를 노드, 외래 키를 엣지로 볼 수 있음
- 그래프는 연결 리스트, 이진 트리, 해시 테이블의 일반화로 볼 수 있음
- 비즈니스 로직에서도 논문 인용 관계, 교통망 경로, 소셜 네트워크 연결 같은 그래프가 자주 등장함
- 그래프는 자주 필요하지만, 주류 언어 대부분은 그래프를 내장 타입으로 제공하지 않고 표준 라이브러리에 포함한 경우도 드묾
- 많은 생태계에는 견고한 서드파티 그래프 라이브러리도 부족해 직접 구현해야 하는 경우가 많음
그래프 타입 설계에는 선택지가 너무 많음
- 그래프에는 방향 그래프와 무방향 그래프 외에도 여러 변형이 있음
- 두 노드 사이 엣지가 최대 하나인 단순 그래프와 여러 엣지를 허용하는 멀티그래프
- 하나의 엣지가 3개 이상의 노드를 연결하는 하이퍼그래프
- 엣지가 다른 엣지를 가리킬 수 있는 우버그래프
- 각 변형마다 추가 설계 결정이 따라붙음
- 엣지에도 ID를 줄지, 노드에만 줄지 정해야 함
- 노드와 엣지에 어떤 데이터를 저장할지도 결정해야 함
- 모든 그래프를 “방향 하이퍼우버멀티그래프” 같은 범용 타입으로 제공하고 사용자가 제한하게 할 수도 있지만, 곧바로 두 가지 문제가 생김
- 연산 결과가 단일 값인지 리스트인지처럼 인터페이스가 달라짐
- 특수한 그래프 성질을 활용하지 못하면 알고리듬 성능이 나빠짐
- 예를 들어 maximum weight matching은 그래프가 이분 그래프임을 알면 빠른 알고리듬을 쓸 수 있지만, 일반 그래프에는 더 느리고 범용적인 알고리듬이 필요함
- 어떤 문제 P, 그래프 G, 알고리듬 A·B·C가 있을 때 어떤 알고리듬을 실행할지 고르는 알고리듬 디스패치 문제도 생김
- 완벽한 그래프 라이브러리는 많은 그래프 종류를 지원해야 하지만, 그만큼 실제 사용자가 원하는 알고리듬 구현에 쓸 시간이 줄어듦
- 그래프 알고리듬은 구현 난도가 높음
- Python 창시자가 작성한
find_shortest_path알고리듬은 이후 다섯 차례 수정됨 - Nicole은 비교한 PageRank 구현이 모두 틀렸다고 말함
- NetworkX는 약 500개의 그래프 알고리듬을 제공하며, 알고리듬 코드만 거의 60,000줄임
- Python 표준 라이브러리 전체는 약 300개 패키지, 600,000줄 미만임
- Python 창시자가 작성한
- 표준 라이브러리 관리자는 어떤 그래프 타입, 어떤 위상 특수 처리, 어떤 알고리듬을 포함할지 결정해야 하므로 유지보수 부담이 큼
- Python도 “batteries included”로 알려져 있지만 PEP 594로 20개 표준 라이브러리 모듈을 제거하는 흐름에 있음
그래프 표현 방식도 하나로 정하기 어려움
- 가장 단순한 방향 그래프만 생각해도 내부 표현은 여러 가지가 가능함
- 엣지 리스트:
[[a, b], [b, c], [c, a], [c, b]] - 인접 리스트:
[[b], [c], [a, b]] - 인접 행렬:
[0 1 0; 0 0 1; 1 1 0] - 서로 참조하는 구조체 집합
- 엣지 리스트:
- 표현 방식에 따라 연산 성능이 달라짐
- 노드 100개, 엣지 200개인 그래프를 인접 행렬로 표현하면 100×100 행렬에 1은 200개, 0은 9,800개가 들어감
- 같은 그래프를 엣지 리스트로 표현하면 노드 쌍 200개만 필요함
- 언어와 최적화 수준에 따라 메모리 차이가 20배 이상 날 수 있음
- 반대로 노드 100개, 엣지 8,000개인 그래프에서 노드 0과 93 사이 엣지를 찾는 경우는 결과가 달라짐
- 인접 행렬은
graph[0][93]로 O(1) 조회 가능함 - 엣지 리스트는 8,000개 엣지를 순회해야 하므로 O(|edge|) 시간이 걸림
- 인접 행렬은
- 엣지가 적은 그래프는 희소 그래프이고, 거의 모든 엣지가 있는 그래프는 밀집 그래프임
- 외부 데이터에서 그래프를 구성하는 프로그램은 처음에는 희소 그래프였다가 나중에 밀집 그래프가 될 수 있어, 내부 표현에 “항상 좋은 선택”은 없음
- 노드 데이터, 엣지 데이터, 여러 종류의 노드와 엣지를 지원하면 구현 복잡도는 더 커짐
- 서드파티 라이브러리는 대체로 두 방향 중 하나를 택함
- 모든 사용 사례를 포괄하는 풍부한 단일 타입을 제공하되 효율성을 희생함
- 표현 방식별 그래프 타입을 따로 제공하고, 노드·엣지 데이터 관리는 사용자에게 맡김
NetworkX와 Petgraph가 보여주는 트레이드오프
- NetworkX는 노드와 엣지에 임의 데이터를 붙일 수 있도록 그래프를 dict의 dict의 dict 구조로 저장함
- 다른 표현으로 변환하는 함수는 제공하지만, 해당 표현 자체로 직접 작업하는 방식은 제공하지 않음
- Rust의 대표 그래프 라이브러리 Petgraph는
graph,graphmap,matrix_graph처럼 사용 사례별 타입을 제공함 - Bradford는 git 저장소 전체 이력에서 secret을 찾는 보안 도구 Nosey Parker에서 Petgraph를 사용함
- 벤치마크 그래프는 CPython이며, 250,000개 commit과 1,300,000개 object를 포함함
- commit 노드당 엣지는 몇 개뿐이어서 인접 리스트를 선택함
- 여러 표현을 지원하면 알고리듬 추가 비용이 커짐
- 표현마다 별도 알고리듬을 작성하면 유지보수 부담이 3~4배로 늘어남
- 다형 타입 위의 범용 추상화로 작성하면 성능이 낮아짐
- 한 인터뷰이는 직접 작성한 그래프 알고리듬이 범용 알고리듬보다 20배 이상 빠를 수 있다고 추정함
성능 제약이 그래프 라이브러리의 핵심 문제임
- 그래프 알고리듬에는 NP-complete 또는 그보다 어려운 문제가 많음
- Karp의 21개 canonical NP-complete 문제 중 14개가 그래프 문제임
- 그래프 문제는 매우 큰 입력에서 실행될 수 있어, 표현 방식과 구현 세부사항이 실행 가능성을 좌우함
- Bradford는 Nosey Parker에서 각 commit마다 파일시스템 스냅샷을 재구성하기 위해 object graph를 순회해야 했음
- Petgraph의 네 가지 그래프 walker는 해당 사용 사례에 맞게 확장되지 않았음
- 즉석에서 “semi-novel” graph traversal algorithm을 설계했고, 메모리 사용량을 1,000분의 1로 줄임
- Zayenz는 그래프가 너무 커서 전체를 다룰 수 없는 사례로 15 puzzle을 들음
- 해법 탐색은 상태 공간에서 A* search를 실행하는 방식임
- 상태 공간은 20조 개가 넘는 상태를 가짐
- 모든 노드를 생성하면 이미 실패한 상태가 됨
- Zayenz가 관여한 Gecode 제약 솔버의 그래프 추가 연구 프로젝트에서도, 범용 그래프 타입은 문제에 맞춘 표현 선택과 경쟁할 수 없었음
- 그래프 데이터베이스도 복잡한 그래프 알고리듬 실행을 위해 설계됐지만 성능 문제가 남아 있음
- Nicole에 따르면 순회 시 깊이를 제한하지 않으면 전체 그래프를 방문하게 됨
- “3단계 밖으로 나가 경로가 있으면 찾기” 같은 깊이 탐색도 많은 데이터를 방문하게 됨
- Nicole은 그래프 쿼리 성능 컨설팅에서 그래프 데이터베이스를 떠나는 마이그레이션을 주로 수행함
- 한 프로젝트에서는 하나의 계산만 그대로 두고 나머지를 MapReduce 절차로 다시 작성함
- 이해하기는 더 어려웠지만, 밤사이에 실제로 완료될 수 있었음
왜 표준 라이브러리에 그래프가 드문가
- 광범위한 그래프 지원이 드문 이유는 여러 요인이 겹치기 때문임
- 그래프 종류가 많음
- 각 그래프 종류마다 표현 방식이 많음
- 그래프 알고리듬 종류가 많음
- 알고리듬 성능이 표현과 구현 세부사항에 민감함
- 사람들은 매우 큰 그래프에서 매우 비싼 알고리듬을 실행함
- 언어 표준 라이브러리는 너무 많은 설계 결정과 트레이드오프, 유지보수 부담을 떠안아야 함
- 프로그래머가 서드파티 그래프 라이브러리를 피하는 이유도 있음
- 라이브러리가 너무 제한적일 수 있음
- 범용 라이브러리가 성능 요구를 만족하지 못할 수 있음
- 그래프는 시스템 분석에는 유용하지만, 구현 단계에서는 데이터 표현과 알고리듬 선택을 직접 통제해야 하는 경우가 많음
부록: 그래프 타입을 제공하는 언어와 관련 도구
- 그래프 질의 언어(GQL)는 그래프 데이터베이스에서 SQL에 해당하는 역할을 함
- GraphQL은 그래프 질의 언어가 아니며, 이름은 Facebook Graph Search와의 연결에서 비롯됨
- GQL과 SQL의 주요 차이는 관계, 즉 “join”이 일급 엔티티라는 점임
- 영화와 사람 데이터셋에서 SQL은 “출연”, “감독”, “제작” 관계를 각각 many-to-many 테이블로 구현함
- SPARQL에서는 관계가 엣지이므로 “영화 Y에서 어떤 역할이든 맡은 사람과 그 역할”을 쉽게 질의할 수 있음
- GQL은 엣지 반전, 합성, 추이 폐쇄 같은 엣지 조작도 지원할 수 있음
- SPARQL은 경로 길이나 경로 중 계산, 예를 들어 두 배우를 잇는 영화 체인 수집은 제공하지 못함
- 이를 지원하는 GQL은 훨씬 복잡해짐
- 형식 명세 언어 Alloy는 relation 데이터 타입에 유용한 그래프 순회 primitive를 갖고 있어, 그래프 표현을 다루기가 일반 프로그래밍 언어보다 쉬움
- 다만 이 primitive들은 라벨이 있는 엣지에 기반하며 다른 그래프 표현에는 맞지 않을 수 있음
- Python은 2020년에 graphlib을 추가함
TopologicalSorter외의 메서드는 없음- 그래프는 노드 dict로만 받음
a -> b그래프를{b: [a]}처럼 반대 방향 dict로 표현함
- 2023년 기준 CPython 내부에서는
graphlib가 사용되지 않음- GitHub에서
graphlib를 참조하는 파일은 900개 미만임 - 같은 해 추가된
zoneinfo는 6,000개 이상 파일에서 등장함 def topological_sort(라는 표현은 4,000개 파일에서 등장함- 직접 구현된 topological sort들은 graphlib와 다른 그래프 표현을 쓰는 경우가 많아 변환하기 어려움
- GitHub에서
- 표준 라이브러리에 그래프 타입이 있는 다른 사례로 Erlang과 SWI-Prolog가 있음
- “모든 것이 그래프”인 프로그래밍 언어도 있음
- Mathematica, MATLAB, Maple 같은 수학 소프트웨어 언어도 어떤 형태로든 그래프 라이브러리를 갖고 있음
- 2024년 3월 18일 업데이트로, 글에 대한 일부 댓글이 별도 페이지에 모였음