족보를 펼치면 한 사람의 내력이 보이고, 계보를 거슬러 오르면 일문(一門)의 흥망이 보인다. 시스템도 그러하다. 태스크는 태스크에 기대고, 라이브러리는 라이브러리를 부르며, 회사는 회사를 거느린다. 이 기댐의 계보 — 의존성(dependency) — 를 그래프로 그려 놓으면, 표를 뒤적여서는 끝내 보이지 않던 것들이 한눈에 들어온다. 과연 아는 만큼 보이는 것이다. 이 장은 그 계보를 읽는 법 — 임팩트 전파, 검증, 단일 장애점, 근본 원인 분석 — 을 답사한다.
답사는 언제나 눈앞의 풍경 셋을 견주는 데서 시작된다. 다음 세 장면에 무슨 공통점이 있는지 잠시 걸음을 멈추고 생각해 보라.
최선의 계획을 세우려면 태스크 사이의 의존을 알아야 한다. "B는 A가 끝나기 전에는 시작할 수 없다"는 B가 A에 의존한다는 말이요, "B와 C를 모두 팀원 X가 수행한다"는 B와 C가 둘 다 X에 의존한다는 말이다.
인기 라이브러리에 취약점이 보고되었다. 내 코드가 그 라이브러리를 직접 쓰는지 확인해야 하고, 그것으로 끝이 아니다 — 내가 고른 라이브러리들이 그 오염된 라이브러리를 저희끼리 쓰고 있지는 않은지까지 살펴야 하는 것이다.
한 회사가 다른 회사 지분의 25%를 갖고, 그 회사가 또 다른 회사의 일부를 갖고… 거래의 최종 수익 소유자 (Ultimate Beneficial Owner)를 찾으려면 소유의 사슬을 끝까지 거슬러야 한다. 금융기관과 규제 당국이 자금세탁·테러자금 방지 점검과 리스크 평가에서 하는 일이 바로 이것이다.
세 풍경의 공통 주제는 의존성 모델링이다. 발상은 지극히 직관적이다 — 개별 직접 의존 하나하나를 노드 사이의 관계로 그리면, 그것들이 얽히며 추이적(transitive) 의존의 네트워크로 자라난다. 이 의존의 네트워크가 곧 그래프이니, 수많은 의존성 문제가 그래프 패턴 매칭과 그래프 알고리즘으로 풀리는 것은 실로 자연스러운 귀결이라 하겠다.
표 11-1은 의존성의 흔한 표현이다. 각 행은 어떤 요소가 다른 요소에 직접 의존함을 적는다. 요소는 네트워크의 라우터일 수도, 공급망의 부품일 수도, 프로젝트의 태스크일 수도 있으나, 그것을 처리하는 방식은 어느 사용 사례에서나 동일하다.
| Element | Depends On |
|---|---|
| A | B, C, D |
| C | H |
| D | J |
| E | F, G |
| F | J |
| G | L |
| H | I |
| J | N, M |
| L | M |
표 11-1 · 의존성 집합의 테이블 표현
이 표를 앞에 두고 "A와 F 사이에 숨은 의존이 있는가?"라는 기초적인 질문을 던져 보라. 답이 안 나오는 것은 아니다 — 행을 짚어 가며 메모를 해 나가면 필경 1분 안에 결론에 이를 것이다: A와 F 사이에는 직접 의존도 간접 의존도 없다. 그러나 이 노고가 어딘가 억울하지 않은가. 표는 계보를 담고 있으되 계보를 보여 주지는 않는 것이다.
SQL로 물으면 사정은 더 딱하다. Example 11-1의 질의는 탐색 두 단계만 내려가는데도 JOIN이 줄줄이 이어지고, 말줄임표가 암시하듯 깊이가 늘수록 JOIN도 늘려 붙여야 한다. 더욱이 "의존이 없다"는 부정의 답은 병리적이다 — 가능한 모든 의존 사슬을 완전히 탐색하기 전에는 결코 유효한 답을 줄 수 없기 때문이다.
SELECT count(*) >0 dependency_exists FROM dependencies d INNER JOIN dependencies d1 ON d.depends_on = d1.elem INNER JOIN dependencies d2 ON d1.depends_on = d2.elem [...] -- 깊이만큼 JOIN을 계속 WHERE d.elem = 'A' AND d2.elem = 'F' -- SQL과 관계형 기술은 재귀 경로 분석을 위해 -- 설계되지 않았다 — 작은 규모에서도 잘 통하지 않는다
LOAD CSV WITH HEADERS FROM "file:///dependencies.csv" AS row MERGE (a:Element { id: row.element }) MERGE (b:Element { id: row.depends_on}) MERGE (a)-[:DEPENDS_ON]->(b) // 이제 같은 질문이 한 줄이 된다 — // 어떤 깊이에도 수정 없이 통하는 가변 길이 표현 MATCH path = (:Element { id: 'A'}) -[:DEPENDS_ON*]->(:Element { id: 'F'}) RETURN path
지식 그래프로 옮기면(그림 11-1) 표의 각 행은 방향 있는 관계가 되고, 의존은 명시적이 된다.
같은 질문 — A와 F 사이에 숨은 의존이 있는가 — 이 이제 하찮은 일이 된다. 사람이라면 몇 초면 A와 F 사이에
의존이 없음을 정확히 짚어낼 것이요, 그래프 데이터베이스는 같은 일을 훨씬 빠르게, 훨씬 큰 규모로 해낸다.
DEPENDS_ON* 패턴 하나가 임의 깊이의 순회를 명한다. 이 견고함에 더해 패턴 기반
질의는 눈에 띄게 직관적이고 간결하니, 소프트웨어 개발의 맥락에서는 곧 유지보수 비용의 절감을
뜻한다. 가히 개종이라 할 만한 전환인 것이다.
의존이 늘 단순한 것은 아니다. 표 11-1처럼 있고 없음만 따지면 족할 때도 있으나, 더 복잡한 문제는 더 많은 데이터를 청한다 — 의존이 얼마나 결정적인지, 얼마나 비싼지, 여러 의존이 어떻게 합쳐지는지. 데이터 모델링의 관점에서 속성 그래프 모델은 관계 타입과 그 안의 속성을 결합해 한정된(qualified) 의존을 표현하는 자연스러운 길을 내어 준다.
그림 11-2의 풍경을 보라. 기업 소유 그래프에서는 한 회사가 다른 회사의 일부를 가진다는 사실만이 아니라
소유의 백분율을 알아야 하고, 용량 활용 그래프에서는 상위 서비스가 하위 서비스 용량의 몇
할을 쓰는지 알아야 한다. OWNS의 part 속성과 CONSUMES의
capacity 속성이 바로 그것을 새긴다. 속성은 절댓값이든 백분율이든 대개 수치이며(하다못해 열거 목록의 값이라도),
여러 값이 합성 가능(composable)해야 한다는 것이 요체다 — 뒤에서 볼 복합 임팩트 전파의 계산과
모델 정합성 검증이 모두 여기에 기대기 때문이다.
의존의 또 다른 결은 시간적 유효성이다. 의존이 언제 유효한지를 새겨 두면, 임의의 일시가 주어졌을 때 그 의존이 활성인지 아닌지 판정할 수 있어야 한다. 그림 11-3이 이 경우인데, 방법은 세기를 새길 때와 똑같다 — 관계 속성에 유효 기간의 시작과 끝을 적는 것이다. 이렇게 역사화된(historized) 의존성 그래프의 질의에는 타임스탬프 매개변수 t가 따라붙어, 시각 t에 활성이던(또는 활성일) 의존만을 탐색하도록 질의를 맥락화한다. 미래를 미리 적어 두지 말라는 법도 없다 — 자원을 미리 예약하는 스케줄링 애플리케이션을 떠올려 보면 될 일이다.
그림 11-2와 11-3에서는 의존하는 엔터티가 모두 단 하나의 나가는 관계로 단 하나의 상대에 기댔다. 그러나 세상일이 어디 그러한가. 투자자는 한 자산 유형에의 노출을 줄이려 투자를 여기저기 벌려 놓고, 공급망은 회복력을 위해 운송 경로를 여럿 낸다(그림 11-4). 다중 의존은 오히려 상례인 것이다. 문제는 그 해석이다 — 논리가 더해지는(additive) 것인가, 아니면 하나만 살아 있으면 되는(at least one) 것인가. 이것이 다중 의존의 두 기본 문법, 가산적/집계형과 중복적/보호형이다.
가산적 의존은 동시에 활성인 것으로 해석되어, 의존이 상대들 위에 분산된다. 요소 A는 요소 B에, B가 A에 기여하는 만큼의 가중치로 의존한다. 그림 11-4 왼편의 포트폴리오가 좋은 본보기다 — 한 자산이 전체의 30%를 차지한다면 포트폴리오는 그 자산에 딱 그만큼 의존하는 것이요, 그 투자에 무슨 일이 생기면(좋은 일이든 궂은 일이든) 포트폴리오에 미치는 영향은 0.3으로 가중된다. 부정적 임팩트라면(의존성 분석의 주된 관심사가 대개 이쪽이다) 전파되는 피해가 그 몫으로 제한된다. 여러 물리 링크의 용량을 본딩한 논리 통신 링크, 여러 물리 드라이브가 받치는 가상 저장 드라이브도 같은 논리를 따른다 — 하나가 무너지면 상위 요소의 용량은 그 기여분만큼 깎이는 것이다. 물론 기여 손실이 쌓이다 보면 상위 요소의 공급이 통째로 무너지는 티핑 포인트가 있을 수도 있다.
중복적 의존은 내결함성을 준다. 중복 집합의 상대들 중 어느 한 시점에 활성인 것은 하나뿐이고 나머지는 필요할 때까지 예비로 대기한다. 상대 중 하나라도 살아 있는 한 의존하는 쪽은 영향을 받지 않는다. 그 결과 어떤 실패는 보호 노드(중복 상대를 여럿 거느린 노드)에 흡수되어 더는 전파되지 않는다. 연중무휴로 돌아야 하는 핵심 서버를 다른 서버가 받치는 고가용성 아키텍처가 그러하고, 방송·통신망이 두 지점 사이에 대체 경로를 여럿 두는 것이 그러하다. 공급망도 마찬가지다 — 그림 11-4 오른편에서 싱가포르와 펠릭스토를 잇는 선호 경로는 수에즈 운하를 지나지만, 시베리아 횡단 철도를 타는 대안도 마련되어 있다. 에너지망과 프로젝트 계획 또한 중복 의존의 무대이니, 이런 지식 그래프의 쓸모는 실로 광대하다 하겠다.
더 복잡한 시스템에는 부모가 작동하기 위한 최소 상대 수가 있을 수 있다. 그 문지방을 넘어서면 보장은 무너진다. 원리는 그대로되, 임팩트 계산식이 임의의 임계값을 반영하도록 일반화될 뿐이다.
완전한 기능을 위해 서버 과반의 가용을 요구하는 데이터베이스 클러스터 — 내결함성 갱신을 위한 쿼럼 — 가 좋은 예다. 그림 11-7에서 GraphDB 노드는 서버 1·2·3에 3중으로 의존하고, 서버들은 저마다 하부 가상 머신에 의존한다. 임계값은 GraphDB 노드의 속성에 담겨 프라이머리 셋 중 둘이 살아 있어야 가용임을 새긴다. 그 위로는 두 recommender API가 GraphDB에 직접 의존하여, 하부 의존이 추이적으로 충족될 때 — 즉 데이터베이스와 그 인프라가 가용할 때 — 최종 사용자에게 기능을 제공한다. 여기서 설명한 두 해석은 기초 두 갈래일 뿐, 실제 시스템의 의존성 그래프에서는 둘이 한 그래프 안에 공존하는 것이 예사다.
이제 두 해석의 다중 의존이 뒤섞인 완전한 의존성 그래프를 분석할 차례다. 데이터셋은 파일 하나 — 매 레코드가
두 요소 사이의 직접 의존과 그 유형('AGG' 가산, 'RED'
중복), 절대 한정치를 적는다(Example 11-4: element / depends_on / mode / abs — "A","B","AGG","80"과 같은
행들). 모든 의존은 ()-[:DEPENDS_ON]->()로 모델링하고, 관계의 mode와 abs 속성이
각각 유형과 가중치를 새긴다.
WITH "file:///qualified-dependencies.csv" AS data LOAD CSV WITH HEADERS FROM data AS row MERGE (a:Element { id: row.element }) MERGE (b:Element { id: row.depends_on}) MERGE (a)-[do:DEPENDS_ON]->(b) ON CREATE SET do.abs = toInteger(row.abs), do.mode=row.mode
일반적 임팩트 분석 질의는 이미 알려진 피해 노드의 목록을 입력으로 받아, 의존성 그래프에 근거한 유도 임팩트로 확장된 목록을 산출한다. 가장 기초적인 것은 잠재 피해 노드 집합의 계산 — 순수하게 위상적인 분석으로, 초기 피해 목록에 대해 직·간접 의존(방향 경로)이 존재하는 모든 노드를 돌려준다. 이 임팩트 범위 분석은 상세 분석을 적용할 부분집합을 먼저 추리는 첫걸음으로 흔히 쓰인다 — 중복·가산 의존을 감안하면 그중 전부가 끝내 피해를 입는 것도, 온전히 입는 것도 아니기 때문이다.
:params declared: [ "I", "K", "J" ] MATCH (e:Element)-[:DEPENDS_ON*]->(impacted) WHERE impacted.id IN $declared RETURN collect(distinct e.id) AS max_impact_list // 결과 (그림 11-9의 최악 시나리오): // ["H","Q","P","O","N","G","B","A","F"]
:params declared: [ "I", "K", "J" ] , current: "F" MATCH (e:Element { id: $current}) -[d:DEPENDS_ON {mode:"RED"}]->(dependee) WITH e.id AS element, dependee.id AS dependee, CASE WHEN dependee.id IN $declared THEN 1 ELSE 0 END AS partial_impact RETURN element, min(partial_impact) AS derived_impact // F → derived_impact 1 // (실용을 위해 threshold 1, 임팩트는 0/1 이진 가정)
:params declared: { "I": 0.5, "K": 1, "J": 0.33 } , current: "G" // 입력이 맵으로 확장된다 — 키는 요소 ID, 값은 0(무피해)~1(전손) 사이의 심각도 MATCH (e:Element { id: $current })-[d:DEPENDS_ON {mode:"AGG"}]->() WITH e.id AS element, coalesce($declared[endNode(d).id],0) * d.abs AS partial_impact RETURN element, sum(partial_impact) AS derived_impact // G → 6.6000000000000005 // 선언되지 않은 상대는 coalesce로 0 처리 — 이 일반 전파 규칙의 Cypher 구현은 // 사실상 어떤 의존성 모델링 시나리오의 세부로도 확장 가능한 견고한 토대다
그림 11-5와 11-6은 이 계산의 정경을 그린다 — 관측·탐지된 임팩트는 진술(statement)로 그래프에 들어와 돋보기 아이콘으로 표시되고(하드 드라이브가 죽었다, 자산 X의 가치가 50% 떨어졌다, 링크 X에 신호 손실이 있다), 유도된 임팩트는 산술의 근거와 함께 계산기 아이콘으로, 전파 경로는 배경 음영으로 드러난다. 진술에서 유도로, 유도에서 다시 유도로 — 파문이 번지듯 임팩트가 계보를 타고 흐르는 것이다.
좋은 유물은 감정을 견딘다. 의존성 그래프 모델의 중요한 미덕 하나는 정합성 검증이 쉽다는 것이다. 잘 빚어진 의존성 그래프가 지켜야 할 조건은 시나리오마다 다르지만, 그 위에 특수 조건을 쌓아 올릴 토대가 될 만큼 일반적인 검증이 넷 있다.
엄밀히 말해 의존성 그래프는 방향 비순환 그래프(DAG)다. 순환이 있다는 것은 어떤 구성 요소가 돌고 돌아 결국 저 자신에 의존한다는 뜻이니, 계산의 관점에서 곤란함은 물론이요 지식 그래프나 도메인 자체의 버그를 반영하는 것이다. 다행한 소식은 그래프가 구조를 명시적으로 만든다는 것 — 순환이든 무슨 모양이든, 그 모양을 질의 패턴으로 그리기만 하면 탐지가 된다. 순환이란 같은 노드에서 시작해 같은 노드에서 끝나는 경로이므로, 패턴도 꼭 그렇게 쓴다.
MATCH cycle = (e:Element)-[d:DEPENDS_ON*]->(e) RETURN cycle
큰 그래프에서 이 질의는 비쌀 수 있다. 그럴 때는 범위를 좁힌다 — 노드 부분집합을 입력으로
주고 그 둘레에서만 순환을 찾거나(WHERE e.id IN $node_subset 필터 추가),
탐색 깊이를 제한한다 — 별표는 "임의 길이"지만
()-[d:DEPENDS_ON*3..45]->처럼 최소·최대를 지정할 수 있다.
모델이 이상한 상황을 그리지 않도록 하는 검증이다. 주주 지분을 합치면 정확히 100%가 되어야 하고, 20Gbps 회선 다섯을 본딩한 광전송 링크는 100Gbps로 보고되어야 마땅하다. 논리는 단순하다 — 가산 집합의 성분 가중치가 상대값이면 합이 1(100%)이어야 하고(Example 11-13), 절대값이면 모델에 노드별 총량이 속성으로 있을 터이니 그 total과 견주면 된다(Example 11-14).
MATCH (e:Element)-[d:DEPENDS_ON { mode:'AGG'}]->() WITH e, sum(d.rel) AS agg_sum RETURN e.id AS element_id, agg_sum = 1 AS valid
MATCH (e:Element)-[d:DEPENDS_ON { mode:'AGG'}]->() WITH e, sum(d.abs) AS agg_sum RETURN e.id AS element_id, agg_sum = e.total AS valid
의존이 수시로 붙고 떨어지는 고도로 동적인 시나리오에서는 현재 총량을 속성으로 유지하는 일이 비싸질 수 있고, 관계의 증감과 용량 갱신이 별개 트랜잭션으로 이루어지면 불일치의 씨앗이 되기도 한다. 용량을 담을 속성이 없다면, 임의 시점의 정합성을 확인할 길은 들어오는 의존과 나가는 의존의 균형을 재는 것뿐이다. Example 11-15가 바로 그 저울이다.
MATCH (e:Element) OPTIONAL MATCH ()<-[dependee:DEPENDS_ON { mode:'AGG'}]-(e) WITH e, sum(dependee.abs) AS agg_sum OPTIONAL MATCH ()<-[dependee:DEPENDS_ON { mode:'RED'}]-(e) WITH e, agg_sum, coalesce(min(dependee.abs),0) AS red_sum WITH e, agg_sum, red_sum, agg_sum + red_sum AS total_cap WHERE total_cap > 0 MATCH (e)<-[dependent:DEPENDS_ON]-() WITH e.id AS elem, agg_sum, red_sum, total_cap, sum(dependent.abs) AS used RETURN elem, agg_sum, red_sum, total_cap, used, used *100.0 / total_cap AS percentage_used
| elem | agg_sum | red_sum | total_cap | used | percentage_used |
|---|---|---|---|---|---|
| B | 0 | 80 | 80 | 80 | 100.0 |
| H | 120 | 0 | 120 | 120 | 100.0 |
| F | 0 | 180 | 180 | 160 | 88.88888888888889 |
| G | 80 | 0 | 80 | 80 | 100.0 |
그림 11-8의 그래프에 적용한 결과가 위의 표(Example 11-16)다. percentage_used가 100%를 넘지 않으면 그 요소의 들고 나는 의존은 정합하다. 정합성 판정에 그치지 않고, 이 보고서는 자원 활용을 최적화할 비즈니스 수준의 지침까지 겸하니 일석이조라 하겠다.
임계값이 정의된 중복 의존 시나리오에서 중복 집합의 성원 수는 최소한 임계값과 같아야 한다 — 그러지 않으면 그 의존은 영영 충족될 수 없다. 그림 11-7의 클러스터에서 PrimaryServer가 하나뿐이라면 임계값 2는 정의상 채울 수 없고, GraphDB 노드는 피해를 입으며 그 피해는 의존망을 타고 번질 것이다.
MATCH (e:Element)-[d:DEPENDS_ON { mode:'RED'}]->() WITH e, count(d) AS available_redundant_elements RETURN e.id AS element_id, available_redundant_elements > e.threshold AS valid
검증이 들춰낸 문제는 환경의 실제 이슈(클러스터가 잘못 프로비저닝됨)일 수도, 데이터 인제스천·그래프 구축 파이프라인의 문제일 수도 있다. 어느 쪽인지 가려내는 일이 후속 조치를 정하는 데 반드시 필요하다. 이 밖에도 추가 검증은 얼마든지 돌릴 수 있다 — 그래프 표현과 Cypher의 표현력이 만나면, 패턴 기반 검증식을 쓰는 일이 이토록 수월한 것이다.
앞서 본 전파 규칙들은 알고리즘적으로 결합되어 하나 또는 여러 실패 노드의 상세 임팩트 평가를 산출한다. 이는 종종 이벤트가 그래프로 스트리밍되는 동안 계산되어 실시간 이벤트 보강에 쓰이며, 복잡한 시스템 위 대량 이벤트의 필터링과 우선순위화에 유용하다. IT 인프라에 무슨 일이 생겼을 때, 보호가 되어 있어 낮은 우선순위인지, 핵심 서비스를 때리고 있어 높은 우선순위인지 — 무엇을 먼저 보고 무엇을 무시할 것인가. 의존성 지식 그래프는 계획을 위한 단일 장애점 탐지와 사후 개선을 위한 근본 원인 분석 같은 문제를 풀도록 돕는다.
단일 장애점이란 시스템 전체가 기대는 단 하나의 요소로, 그것이 무너지면 시스템이 무너진다. 원리는 의존성 그래프의 부분들로 일반화된다. 그림 11-7을 다시 보라 — Primary2와 Primary3 두 노드가 같은 CloudVM 노드(VM2)에 직접 의존하고 있으니, GraphDB(recommender DB)는 실은 VM2에 강하게 의존하는 셈이다. 실생활에서 흔한 실수가 바로 이것이다. 내결함성 소프트웨어의 인스턴스 여럿을 같은 하드웨어 위에 배치하면 하드웨어 장애 한 번에 대형 사고를 맞는다. 가상 머신만의 얘기가 아니다 — 네트워크, 냉각, 전력, 심지어 데이터센터 전체까지 모든 인프라가 해당된다. 말하자면 기저의 SPOF 의존 때문에 내결함성이 겉치레에 그치는 것이다. 시스템을 설계할 때, 투자를 집행할 때 이런 정경은 곧잘 눈에 띄지 않고 지나가지만(실제로 자주 그런다), 지식 그래프를 쓰면 지나칠 수가 없다.
위상적으로 SPOF는 그래프의 또 하나의 패턴에 불과하다. 그러니 Cypher 패턴으로 그려서 의존성 그래프 안의 모든 사례를 표면화하면 된다 — 주어진 요소에서 출발한 여러 가변 길이 의존 사슬(경로)이 어느 한 요소 spof로 수렴하는 모양이다.
MATCH alertPath = (spof)<-[:DEPENDS_ON*]-(e:Element) -[:DEPENDS_ON*]->(spof) WHERE e.id = $selected_node_id RETURN alertPath
궁극의 임팩트는 가산·중복 다중 의존을 감안한 상세 분석으로 가려야 하고, 부분 실패는 용인될 수도 있다. 그러나 SPOF가 대체로 달갑잖은 패턴임은 분명하니, 풀 가치가 없더라도 최소한 감시는 해야 한다. 지리가 병목(SPOF)을 강요하는 에너지망을 생각해 보라 — 풀 수 없는 지형이라 해도, 그것이 전체 망에 어떤 영향을 주는지 알아야 병원 같은 핵심 시설의 백업 디젤 발전 같은 대응책을 제자리에 둘 수 있는 것이다.
근본 원인 분석은 임팩트 전파의 역문제다. 피해의 모음이 주어졌을 때, 그것들이 모두 단 하나의 요소에 의존하는지를 캐고, 그렇다면 그 공통 조상이 관측된 피해들의 근본 원인이라는 결론에 이른다. 이 분석이 도드라지는 무대가 서비스 어슈어런스 시스템이다 — 복잡한 시스템(통신망, 공급망, IT 인프라…)을 감시하며 구성 요소들로부터 알람·이벤트·지표를 끊임없이 받는데, 어떤 오작동은 요소 고유의 것이지만 어떤 것은 의존하는 요소의 실패가 번져 온 것이다. 감시 시스템은 알람의 홍수를 고신호/저잡음으로 줄여 개입을 정확히 이끌어야 하니, 유도된 알람들을 근본 원인으로 묶어 근본 원인만 제시하는 것이 한 방법이다 — 이른바 알람 상관(alarm correlation), 이벤트 상관이며, 알람의 직·간접 의존을 캐는 일임을 이제는 직감할 것이다.
첫걸음은 후보의 식별이다. 피해 노드의 모음(증상)이 주어지면, 그래프의 의존을 탐사해 증상 집합의 부분 집합을 설명할 수 있는 리프 노드 — 남에게 의존하지 않아 근본 원인일 수 있는 노드들 — 를 찾는다. 이 탐사는 서로소 집단들을 즉시 드러낸다 — 동시에 일어났으되 서로 무관한 결함들이 따로따로 묶이는 것이다.
:param symptoms: ["N","O","P","H","I","A","E"] MATCH (e:Element)-[:DEPENDS_ON*0..]->(x) WHERE e.id in $symptoms AND NOT (x)-[:DEPENDS_ON]->() WITH x, collect(distinct e.id) AS explains_faults WHERE size(explains_faults) > 1 RETURN x.id AS candidate_root, explains_faults ORDER BY size(explains_faults) desc // NOT (x)-[:DEPENDS_ON]->() : 리프 = 근본 원인 후보. // collect(distinct e.id)가 각 후보로 설명되는 // 증상들을 모은다
| candidate_root | cluster |
|---|---|
| I | ["H","N","O","P","I"] |
| K | ["H","N","O","P","A"] |
| J | ["H","N","O","P","A"] |
| E | ["E","A"] |
그림 11-8의 그래프에 돌린 결과(Example 11-20)다. 서로 독립인 두 군집이 한눈에 보인다 — E가 설명할 법한 군집과 I·K·J가 설명할 법한 군집이다. 이제 둘째 걸음에서 정보 검색의 낯익은 지표 — 정밀도(precision), 재현율(recall), F-점수 — 로 후보들의 서열을 매긴다.
증상 목록의 요소 중 이 후보가 설명하는 몫은 얼마인가?
이 후보가 진짜 원인이라면 최대 임팩트는 어디까지였겠는가 — 그것이 실제 관측(초기 실패 목록)과 얼마나 부합하는가?
정밀도와 재현율에서 계산되는 성능 지표. 균형 잡힌 후보가 이긴다.
MATCH (e:Element)-[:DEPENDS_ON*0..]->(x) WHERE e.id IN $symptoms AND NOT (x)-[:DEPENDS_ON]->() WITH x, collect(distinct e.id) AS explains_faults WHERE size(explains_faults) > 1 WITH x AS candidate_root, explains_faults MATCH (candidate_root)<-[:DEPENDS_ON*0..]-(x) WITH candidate_root, explains_faults, collect(distinct x.id) AS potential_max_impact WITH candidate_root, toFloat(size(explains_faults))/size($symptoms) AS precision, toFloat(size([x in potential_max_impact WHERE x in $symptoms])) / size(potential_max_impact) AS recall RETURN candidate_root.id, precision, recall, (2 * precision * recall) / (precision + recall ) AS fscore ORDER BY fscore DESC
판독은 이러하다. I·J·K의 정밀도는 매우 비슷하다 — 셋 다 탐지된 증상의 상당 부분을 일으켰을 수 있다는 뜻이다. 그러나 J가 근본 원인이었다면 J에 간접 의존하는 노드가 훨씬 많으니 증상 목록에 실패 노드가 훨씬 많이 올라왔어야 한다. 그것들이 입력에 없다는 사실이 J의 혐의를 던다. K도 마찬가지다. 오직 I만이 정밀도와 재현율의 균형 잡힌 조합을 갖추었고, 그것이 F-점수에 새겨진 것이다. 물론 이 접근은 관측된 증상의 품질에 기댄다 — 목록이 온전할수록 결과가 정확하다. 현실에서 환경의 현재 상태를 빠짐없이 아는 일은 드물기에, 이런 분석의 결과는 단 하나의 답이 아니라 가장 유력한 후보들의 목록이 되기 마련이다. 그러나 그 결과를 시간이나 여타 도메인 특화 휴리스틱과 결합하면 올바른 진단의 확률은 얼마든지 높일 수 있다.
Vanguard Group은 세계 최대급 자산운용사다 — 뮤추얼 펀드 최대 제공자요, ETF 제2위 제공자다. Vanguard는 기존 모놀리식 Java 시스템을 마이크로서비스 아키텍처로 리팩터링하는 도전에 직면해 있었고, 그러자면 모놀리스의 모든 구성 요소와 — 무엇보다 — 구성 요소 사이의 상호 의존에 대한 가시성이 필요했다. 레거시 Java 코드 모듈 중에는 400만 라인에 이르는 것도 있었으니, 소프트웨어의 지원 없이는 감당이 불가능한 일이었다. 게다가 그 과정에서 죽은 코드를 쳐내 기술 부채를 줄이는 일도 원했다. 팀은 자신들이 마주한 것이 그래프 문제임을 알아보았고, 분석 요구의 규모와 복잡성은 지식 그래프에 안성맞춤이었다. 그들은 빌드 프로세스의 일부로 지식 그래프 구축을 자동화하여 기존 코드 아티팩트 전부를 의존성과 함께 담았고, 빌드에서 "발견된 그래프"에 더해 아키텍처 다이어그램의 정보를 얹어 어긋남(misalignment)을 탐지했다. 이제 팀은 그래프 분석과 시각적 탐사로 코드를 모범 사례에 견주어 평가하고, 서비스 간 호출 수를 제약해 리스크와 지연을 줄이는 등의 모범 사례를 강제할 핵심 지표를 도출한다. 그래프에서 유도한 지표로 코드 품질 스코어카드도 만들어져 비즈니스 전반의 협업을 가능케 하고 있다.
의존성을 포착하고 활용하는 것은 흔하고도 중요한 능력이다. 의존성을 표현하고 처리하는 자연스러운 길은 의존성 중심 지식 그래프를 쓰는 것이니, 그 표현력과 직관성과 효율 때문이다. 이 장의 아이디어들은 임팩트 분석과 근본 원인 분석 같은 복잡한 문제를 풀 의존성 중심 지식 그래프 구축의 기본 원리를 보여 주었고, 예제들은 이 그래프의 폭넓은 적용 가능성을 일러 주었다 — 여기서부터는 이 기법들을 자신의 필요에 맞게 적용하고 확장하면 되는 것이다. 답사의 끝에서 새삼 확인하는 진실은 하나다. 계보는 그려야 보이고, 보여야 지킬 수 있다.
세 풍경, 하나의 문법. 태스크 계획·취약 라이브러리·UBO 소유 사슬은 모두 의존성 모델링이다. 직접 의존은 관계가 되고, 추이적 의존의 네트워크는 그래프가 되며, SQL의 재귀 JOIN 지옥은 DEPENDS_ON* 한 줄로 갈음된다 — 특히 "없다"는 부정의 답에서 격차가 벌어진다.
의존에는 결이 있다. 세기(소유 %·용량)는 합성 가능한 수치 속성으로, 세월(유효 기간)은 start/end 속성으로 새기고 타임스탬프 t로 질의를 맥락화한다 — 미래의 예약까지 담을 수 있다.
다중 의존의 두 문법. 가산(parent = Σ child·weight — 포트폴리오·본딩 링크·티핑 포인트)과 중복(parent = min(children) — 보호 노드·대체 경로·수에즈 vs 시베리아), 그리고 임계값 일반형(쿼럼). 실제 그래프에서는 둘이 공존한다.
감정 네 가지. ① 순환 없음(DAG — 범위·깊이로 비용 제어), ② 가산 합계 = 총량(주주 100%·5×20Gbps), ③ 소비 ≤ 생산(동적 시나리오의 균형 저울, 자원 최적화 지침 겸용), ④ 중복 성원 수 ≥ 임계값. 발견된 흠이 유물의 것인지 탁본의 것인지 — 환경 문제인지 파이프라인 문제인지 — 를 가려야 한다.
SPOF는 패턴이고, 근본 원인은 역문제다. 같은 VM 위의 다중 프라이머리처럼 겉치레 내결함성은 수렴 경로 패턴으로 들통난다. RCA는 리프 후보 군집화 → precision/recall/F-score 서열화 — 증상 품질에 기대되, 시간·도메인 휴리스틱과 결합해 진단 확률을 높인다. Vanguard는 이 계보학으로 400만 라인 모놀리스를 해부했다.
다음 답사지는 문서의 세계다. 세상 데이터의 좋은 몫은 인간이 인간을 위해 자연어로 쓴 문서인데, 자연어는 표나 계층 같은 구조가 없어 프로그램으로 부리기 어렵다. NLP가 뽑아낸 구조가 그래프에 얼마나 자연스럽게 안착하는지, 비정형 데이터로 세운 지식 그래프가 어떤 정교한 활용을 여는지 — 시맨틱 검색과 유사도(12장, Semantic Search and Similarity)에서 만난다.