ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 값은 어디서 와서 어디로 가는가 — 데이터흐름 그래프(DDG)로 코드 읽는 법 배우기
    IT 2026. 7. 3. 22:00
    값은 어디서 와서 어디로 가는가 — 데이터흐름 그래프(DDG)로 코드 읽는 법 배우기

    들어가며 — 코드를 읽다가 매번 막히는 그 질문

    남이 짠 코드를 읽다 보면 거의 반사적으로 떠오르는 질문이 하나 있다. "이 변수의 값, 도대체 어디서 온 거지?" 함수 한가운데에 total이라는 변수가 떡하니 쓰이고 있는데, 그 값이 어느 줄에서 만들어졌는지, 중간에 누가 바꿔치기했는지를 스크롤을 위아래로 한참 굴려야 겨우 짐작한다. 반대 방향의 질문도 있다. "여기서 계산한 이 값, 나중에 쓰이긴 하나?" 애써 구한 값이 사실은 아무도 안 읽는 죽은 값이라면, 그 계산은 그냥 낭비다.

    이 두 질문이 사실은 같은 뿌리에서 나온다는 걸 알게 된 게 이번 학습의 출발점이었다. 둘 다 "값의 흐름"을 묻고 있다. 어디서 만들어져(정의), 어디로 흘러가(사용) 닿는가. 이 흐름을 사람이 머릿속에서 추적하는 대신 그래프로 그려둔 것이 바로 데이터흐름 그래프(Data-Flow Graph), 혹은 데이터 의존 그래프(Data Dependence Graph)다. 줄여서 DDG라고 부른다. 이 글은 내가 이 개념을 처음부터 더듬어가며 정리한 학습 노트다.

    DDG란 무엇인가 — 값의 흐름을 그린 지도

    DDG는 한 문장으로 이렇게 말할 수 있다. "한 곳에서 만든 값이 다른 곳의 변수에 영향을 준다"는 값의 흐름을 그린 그래프. 그래프이니 노드와 엣지로 이뤄지는데, 각각의 의미가 명확하다.

    정의(definition)란 변수에 값을 넣는 지점이다. x = 5처럼 어떤 변수에 값이 들어가는 순간. 사용(use)이란 값을 읽는 지점이다. y = x + 1에서 x를 읽어오는 순간. DDG의 노드는 바로 이 정의가 일어나는 곳, 그리고 사용이 일어나는 곳이다. 그리고 엣지는 "이 정의가 만든 값이 저 사용처에 닿는다"는 데이터 의존을 나타낸다.

    diagram

    다이어그램 설명. 값이 만들어진 지점에서 그 값이 읽히는 지점으로 화살표가 이어진다. 다섯이라는 값을 담은 변수가 다음 줄에서 읽혀 새 계산에 쓰이고, 그 결과가 또 다음 줄로 흘러간다. 화살표 하나하나가 "이 값이 저기서 쓰인다"는 의존을 뜻한다. 흐름은 위에서 아래로, 값이 태어난 곳에서 소비되는 곳으로 향한다. 주의할 점은 이 화살표가 "줄 순서"가 아니라 "값의 출처"를 따라간다는 것 — 코드가 멀리 떨어져 있어도 값이 흐르면 엣지가 생긴다.

    비유하자면 택배 배송 지도 같은 것이다. 어떤 물건이 어느 창고(정의)에서 출발해 어느 집(사용)으로 배달되는가. 코드의 줄 번호 순서는 "내가 오늘 어느 동네를 먼저 들렀나"라는 동선일 뿐이고, DDG는 "이 택배가 정확히 어느 창고에서 와서 어느 집에 도착하는가"라는 출발지-도착지 연결을 그린다.

    배경 — 컴파일러가 먼저 필요로 했다

    이 개념은 갑자기 코드 분석 도구를 위해 생긴 게 아니다. 뿌리는 컴파일러의 데이터흐름 분석(data-flow analysis), 즉 프로그램 안에서 값이 어떻게 전파되는지를 정적으로 계산하는 기법에 있다. 1973년 게리 킬달(Gary Kildall)이 발표한 논문 "A Unified Approach to Global Program Optimization"(POPL 1973)이 전역 데이터흐름 분석을 하나의 통합된 틀로 정리한 출발점으로 꼽힌다.

    왜 컴파일러가 이걸 필요로 했을까. 컴파일러의 일 중 하나는 최적화다. 더 빠르고 더 작은 기계어를 뽑아내려면 코드의 의미를 이해해야 하는데, 대표적인 두 가지 최적화가 값의 흐름 정보를 절실히 요구했다.

    하나는 상수 전파(constant propagation): 어떤 변수가 항상 같은 상수값을 가진다면 그 변수를 아예 상수로 바꿔치기하는 최적화다. x = 5 다음에 y = x + 1이 온다면, 컴파일러는 y = 5 + 1, 더 나아가 y = 6으로 미리 계산해둘 수 있다. 단 이렇게 하려면 "이 사용처에 닿는 값이 정말 항상 5인가"를 알아야 한다. 다른 하나는 죽은 저장 제거(dead store elimination): 값을 만들어 변수에 넣었는데 그 값을 아무도 읽지 않는다면 그 저장 자체를 지우는 최적화다. 둘 다 "값이 어디서 와서 어디로 가나"를 모르면 손도 댈 수 없다.

    어떤 문제 — CFG만으로는 답이 안 나온다

    여기서 먼저 짚어야 할 게 제어흐름 그래프(CFG, Control-Flow Graph): 프로그램이 어떤 순서로 실행되는지, 어디서 분기하고 어디서 합쳐지는지를 그린 그래프다. 컴파일러와 정적 분석의 기본 토대인데, CFG는 강력하지만 한계가 분명하다. CFG는 '실행 순서'는 알려주지만 '어떤 값이 어떤 값에 의존하는지'는 모른다.

    diagram

    다이어그램 설명. 이것은 CFG다. 입력을 받고, 조건을 검사해 두 갈래로 나뉘었다가, 다시 한 지점으로 합류해 출력으로 끝난다. 화살표는 "다음에 어디로 실행이 넘어가는가"라는 순서만 말한다. 하지만 이 그림만 봐서는 마지막에 출력되는 값이 위쪽 곱셈에서 온 것인지 아래쪽 영(0) 할당에서 온 것인지, 그 출처의 연결이 직접 그려져 있지 않다. 흐름의 방향은 실행 순서를 따르며, 함정은 바로 여기 — 순서를 안다고 출처를 아는 게 아니다.

    CFG가 답하지 못하는 질문을 다시 적어보면 이렇다. "이 변수의 값은 어디서 왔나? 이 값이 나중에 쓰이긴 하나?" 실행 순서를 줄줄 따라간다고 해서 이 질문이 저절로 풀리지 않는다. 분기와 반복이 얽히면, 한 사용 지점에 도달할 수 있는 값의 출처가 여러 군데일 수도 있고, 멀리 떨어진 곳일 수도 있다. 사람이 매번 눈으로 추적하기엔 금방 한계가 온다.

    어떻게 해결하나 — 도달 정의와 정의-사용 연쇄

    해결의 핵심은 이렇다. CFG 실행 흐름 위에서 각 변수의 정의와 사용을 계산하고, 한 정의에서 그 값이 도달하는 사용처로 데이터 의존 엣지를 잇는다. 즉 실행 순서 그래프를 토대 삼아, 그 위에 값의 흐름을 새 층으로 얹는 것이다. 이 계산의 중심에 도달 정의(reaching definitions): 어떤 정의가 특정 사용 지점까지 살아서 도달하는가를 따지는 분석이 있다.

    "살아서 도달한다"는 표현이 핵심이다. 어떤 변수에 값을 넣었더라도, 그 값이 쓰이기 전에 다른 정의가 같은 변수를 덮어쓰면(redefine) 앞의 정의는 그 지점까지 "도달하지 못한다." 마치 칠판에 글씨를 썼는데 누가 그 위에 다시 다른 글씨를 덮어쓰면 원래 글씨는 더 이상 읽히지 않는 것과 같다. 도달 정의 분석은 각 사용 지점에서 "지금 이 변수에 살아있는 정의가 어떤 것들인가"를 계산한다.

    diagram

    다이어그램 설명. 같은 변수에 두 번 값을 넣은 뒤 그 변수를 읽는 상황이다. 점선은 첫 번째 할당이 읽기 지점까지 살아서 닿지 못함을, 굵은 화살표는 두 번째 할당의 값이 실제로 닿음을 보여준다. 흐름은 위에서 아래로, 그러나 "닿는다/못 닿는다"는 중간에 덮어쓰기가 있었는지에 따라 갈린다. 함정은 코드 줄 순서만 보면 두 정의가 다 위에 있지만, 읽기 지점에 실제로 도달하는 건 마지막 것 하나뿐이라는 점이다.

    이렇게 "어떤 정의가 어떤 사용에 도달하는가"를 계산해 그 둘을 엣지로 이으면, 정의-사용 연쇄(def-use chain): 하나의 정의에서 출발해 그 값을 읽는 모든 사용 지점을 사슬처럼 연결한 것이 만들어진다. 정의 하나가 머리, 그 값을 읽는 여러 사용처가 줄줄이 매달린 사슬이다.

    diagram

    다이어그램 설명. 하나의 정의에서 여러 사용처로 화살표가 부챗살처럼 퍼진다. 가격이라는 값 하나가 세금 계산, 화면 표시, 합계 계산 세 곳에서 읽히고 있다. 이 부챗살 전체가 하나의 정의-사용 연쇄다. 이 그림이 있으면 "가격 정의를 고치면 어디가 영향받나"가 한눈에 보인다 — 화살표가 닿는 세 곳 전부다. 함정은 이 연쇄가 코드상 인접하지 않을 수 있다는 것 — 세 사용처가 파일 곳곳에 흩어져 있어도 의존은 하나로 묶인다.

    효과 — 최적화에서 보안 추적까지

    값의 흐름을 그래프로 손에 쥐면 할 수 있는 일이 확 늘어난다. 학습하며 정리한 효과를 표로 묶어봤다.

    활용 무엇을 하나 DDG의 어떤 정보가 쓰이나
    상수 전파 항상 같은 상수인 변수를 미리 계산해 치환 "이 사용처에 닿는 정의가 항상 같은 상수값인가"
    죽은 저장 제거 아무도 읽지 않는 저장을 삭제 "이 정의에서 출발하는 사용 엣지가 하나도 없는가"
    프로그램 슬라이싱 특정 값에 영향을 주는 코드 조각만 추려내기 정의-사용 연쇄를 거꾸로 거슬러 올라간 데이터 부분
    오염 추적(taint) 위험한 외부 입력이 어디까지 번지는지 따라가기 오염된 정의에서 출발하는 데이터 의존의 전파 경로

    앞서 본 상수 전파와 죽은 저장 제거는 컴파일러 최적화의 직접적 응용이다. 더 흥미로운 건 그다음이다. "이 값이 어디서 와서 어디로 가나"라는 추적 능력은 곧바로 다른 영역으로 번진다.

    프로그램 슬라이싱(program slicing)이란 "이 결과에 영향을 준 코드만 남기고 나머지를 잘라내는" 기법이다. 어떤 변수의 최종 값에 관심이 있을 때, 그 값에 기여한 정의들을 데이터 의존을 따라 거꾸로 거슬러 모으면 관련 코드 조각만 추려진다. 이때 따라가는 사슬이 바로 정의-사용 연쇄다.

    diagram

    다이어그램 설명. 외부에서 들어온 입력값이 변수에서 변수로 복사되며 결국 데이터베이스 실행 지점까지 흘러가는 경로다. 시작점의 입력을 "오염됐다"고 표시하면, 데이터 의존을 따라 그 오염이 어디까지 번지는지가 그대로 드러난다. 흐름은 입력에서 위험 지점으로 향하고, 이 경로가 끊기지 않고 닿는다는 사실 자체가 보안 경고가 된다. 함정은 중간에 값이 검증·정제되는 단계가 끼어 있으면 오염이 "씻기는데", 그 단계를 그래프가 알아채지 못하면 오탐이 생긴다는 점이다.

    이것이 오염 추적(taint analysis): 신뢰할 수 없는 입력이 프로그램 안에서 어디까지 퍼지는지를 따라가는 보안 분석이다. 사용자 입력처럼 위험할 수 있는 값을 "오염원"으로 찍어두고, 데이터 의존을 따라 그 오염이 데이터베이스 질의나 명령 실행 같은 "위험 지점"에 닿는지를 본다. 닿으면 보안 취약점 후보다. 컴파일러 최적화에서 출발한 값의 흐름 그래프가, 결국 보안 분석의 토대까지 떠받치는 것이다.

    DDG의 자리 — CFG 위, PDG 아래

    여러 그래프가 나와서 헷갈리기 쉬운데, 셋의 관계를 정리하면 머릿속이 깔끔해진다. 비교를 위해 블록을 나눠 본다.

    ▲ 제어흐름 그래프 — 실행 순서의 층

    diagram

    다이어그램 설명. 제어흐름 그래프는 "다음에 무엇을 실행하나"라는 순서와 분기·합류만을 그린다. 화살표는 시간의 흐름, 곧 실행의 진행 방향이다. 값에 대한 정보는 여기 없다 — 오직 "어디로 가는가"만 있다.

    ▲ 데이터흐름 그래프 — 값의 흐름 층

    diagram

    다이어그램 설명. 데이터흐름 그래프는 CFG에 "값이 어디서 와서 어디로 가나"를 얹은 층이다. 화살표는 시간이 아니라 데이터 의존 — 정의에서 사용으로 향한다. 같은 코드라도 제어흐름 그래프와는 전혀 다른 모양의 그림이 나오며, 둘은 서로를 대체하는 게 아니라 보완한다.

    ▲ 프로그램 의존 그래프 — 둘을 합친 층

    diagram

    다이어그램 설명. 프로그램 의존 그래프(PDG)는 데이터 의존과 제어 의존을 한 그래프에 합쳐 그린 것이다. 한 종류의 화살표는 "이 조건 때문에 실행된다"는 제어 의존을, 다른 종류는 "이 값을 넘겨받는다"는 데이터 의존을 나타낸다. 슬라이싱처럼 "이 결과에 영향을 준 모든 것"을 묻는 분석은 두 의존을 모두 따라가야 하므로 이 합친 그래프가 필요하다.

    정리하면, DDG는 CFG(실행 순서)를 보완하기 위해 값의 흐름을 다룬 층이다. 그리고 여기에 제어 의존(control dependence): 어떤 문장이 실행될지가 어느 조건의 참·거짓에 달려 있는 관계까지 합쳐지면 프로그램 의존 그래프(PDG)가 된다. CFG는 토대, DDG는 그 위의 한 층, PDG는 데이터와 제어 두 층을 합친 상위 구조. 이 계단을 알고 나니 정적 분석 문서를 읽을 때 길을 잃지 않게 됐다.

    코드 위키 자동생성에 왜 핵심인가

    마지막으로, 내가 이 개념에 끌린 진짜 이유다. 요즘 관심을 두고 있는 게 코드에서 자동으로 설명 문서(코드 위키)를 생성하는 일인데, 그 작업의 한가운데에 바로 이 값의 흐름 정보가 있다.

    코드 위키를 그럴듯하게 자동 생성하려면, 단순히 "이 함수는 이런 줄들로 이뤄져 있다"를 나열하는 것으로는 부족하다. 진짜 가치 있는 설명은 "이 함수가 받은 입력이 내부에서 어떻게 변형되어 무엇으로 흘러나가는가"다. 사람이 코드를 이해할 때 머릿속으로 그리는 그 값의 흐름을, 도구가 DDG로 미리 계산해두면 설명의 근거가 단단해진다.

    diagram

    다이어그램 설명. 소스 코드에서 출발해 실행 순서 그래프를 만들고, 그 위에서 정의와 사용을 계산해 데이터 의존을 이어 값의 흐름 그래프를 얻은 뒤, 그 흐름을 근거로 설명 문장을 짓고 최종적으로 위키 페이지가 된다. 흐름은 코드라는 사실에서 검증 가능한 그래프를 거쳐 설명으로 향한다. 함정은 중간 그래프를 건너뛰고 코드를 바로 글로 옮기면 "왜 그렇게 말하는가"의 근거가 사라진다는 점 — 그래프가 곧 설명의 출처 증거다.

    특히 매력적인 건 추적 가능성이다. "이 설정값이 어디서 읽혀 어떤 결정에 쓰이는가" 같은 문서 한 줄이, 막연한 짐작이 아니라 실제 정의-사용 연쇄에 근거한다면, 그 문서는 코드가 바뀔 때 어디를 다시 써야 하는지도 함께 알려준다. 값이 흐르는 경로가 곧 "이 설명이 의존하는 코드 지점"이기 때문이다. 코드가 진실의 원천이고 문서가 거기서 파생된 산출물이 되려면, 둘 사이를 잇는 검증 가능한 연결이 필요한데, 데이터 의존이 바로 그 연결의 한 축을 맡는다.

    마치며

    처음엔 그저 컴파일러 최적화 기법 중 하나려니 했던 데이터흐름 그래프가, 알고 보니 "값은 어디서 와서 어디로 가는가"라는 단 하나의 질문을 집요하게 파고든 결과물이었다. 그리고 그 질문은 코드를 읽는 사람이라면 매일 던지는 질문이고, 최적화·슬라이싱·보안 추적·문서 자동생성이 모두 그 답을 필요로 한다. CFG가 실행의 줄거리라면 DDG는 값의 족보다. 둘을 겹쳐 보는 눈을 갖추는 것 — 이번 학습의 가장 큰 수확이었다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.