데이터 흐름 테스트로 변수 생애주기 결함 찾기

데이터 흐름 테스트의 Def-Use 체인과 커버리지 기준, 정적·동적 분석 결합 방식 및 CI 운영 전략을 정리합니다.

2026-08-14 · 최초 발행 2025-12-22

변수의 정의와 사용 사이를 검증하는 테스트

데이터 흐름 테스트는 코드 속 변수의 정의(Def), 사용(Use), 소멸(Kill) 지점을 제어 흐름 그래프(CFG)에서 추적하는 기법이다. 사용은 계산에 쓰이는 c-use와 분기 조건에 쓰이는 p-use로 나뉘며, 정의와 사용의 연결은 DU 체인(Def-Use Chain)으로 표현한다. 이 체인을 기준으로 테스트 요건을 만들면 실행 순서는 통과했지만 데이터 상태가 잘못된 결함까지 겨냥할 수 있다.

대표적인 커버리지 기준은 다음과 같다.

  • All-Defs: 각 정의가 적어도 한 번의 사용 지점에 도달하도록 확인한다.
  • All-Uses: 각 정의에서 모든 c-use와 p-use까지 도달하도록 확인한다.
  • All-DU-Paths: 각 DU 체인의 모든 단순 경로를 커버한다.

이 과정에서 정의되기 전에 사용하는 UD(use-after-undef), 사용되지 않은 채 다시 정의되는 DD(double-def), 정의 뒤 사용 지점에 도달하지 못하는 DU 미도달 또는 죽은 정의를 탐지한다.

정적 분석에서는 CFG·PDG, SSA, 에일리어싱 정보를 활용해 후보 경로를 분석한다. 동적 분석은 계측과 테인트 추적으로 실제 실행 경로를 수집한다. 두 방식을 함께 쓰면 정적 분석의 후보 선별과 런타임 검증을 연결할 수 있다.

테스트 요건을 만드는 분석 흐름

변수 생애주기는 정의에서 사용을 거쳐 소멸하는 단위로 모델링한다. c-use와 p-use를 구분하고, 경로에 따라 결과가 달라지는 경우에는 경로 민감(path-sensitive) 분석이 필요하다.

All-Defs, All-Uses, All-DU-Paths 기준은 각각 테스트 요건의 범위를 결정한다. 다만 모든 경로를 그대로 다루면 경로 폭발이 발생할 수 있다. 루프 언롤링 한계를 두고 경계 경로를 샘플링하는 방식으로 분석 범위를 제어한다.

도구 체인은 파서와 CFG/PDG 빌더, DU 추출기, 요건 생성기, 계측기, 러너·리포터로 이어진다. 이 결과를 CI 파이프라인의 커버리지 게이팅과 연결하면 변경 시점에 데이터 흐름 결함 후보를 확인할 수 있다.

정적 분석 시작Def/Use 탐색커버리지 목표 도출도구 선택테스트 실행리포트 생성파싱 실패/코드 생성 감지런타임 수집입력: 소스 코드, 테스트 스위트파서/빌더: CFG/PDG 생성DU 체인 생성테스트 요건: ALL-DEFS /ALL-USES / ALL-DU-PATHS계측기 삽입: 정적/동적실행 로그/커버리지결과: 누락 DU, 결함 후보폴백: 동적 테인트 추적

함수 간(interprocedural) 분석, SSA 변환, 요약(summary)에 기반한 모듈 경계 처리는 규모가 큰 코드베이스에서 필요하다. 증분 분석 캐시와 변경 영향 분석(Diff-Aware)을 적용하면 변경된 영역을 중심으로 검증 범위를 잡을 수 있다.

데이터 결함이 드러나는 지점

결제·정산 모듈을 리팩토링할 때는 금액 정규화 변수에서 발생하는 DD와 DU 이상을 확인할 수 있다. 회귀 테스트에 All-Uses 커버리지 게이팅을 적용하면 정의된 값이 필요한 계산과 분기에 도달하는지를 점검하게 된다.

입력 검증 경로에서는 테인트 추적으로 SQL·명령어 인젝션 후보 경로를 식별할 수 있다. 특히 p-use 분기 지점에서 필터링이 빠졌는지 확인하는 데 활용한다.

데이터 파이프라인과 ETL에서는 Null/NaN 전파, 단위 변환 누락, 중복 정의로 인한 컬럼 오염을 대상으로 한다. 배치 파이프라인에 DU 커버리지 리포트를 통합해 데이터가 어디에서 정의되고 어느 변환 단계까지 전달되는지 볼 수 있다.

제한된 메모리를 쓰는 임베디드·실시간 시스템에서는 초기화 누락(UD) 검출이 중요하다. 인터럽트 경계에서는 에일리어싱을 고려해 All-Defs 커버리지를 우선 적용할 수 있다.

다른 테스트 접근과의 차이

접근법 결함 검출력 확장성 일관성 안정성 운영 편의
데이터 흐름 테스트 높음(데이터 결함 강점) 보통(경로 폭발 관리 필요) 보통(옵션 영향) 보통~높음 보통
제어 흐름 테스트 보통 높음 높음 높음 높음
변이 테스트 매우 높음 낮음(비용 큼) 높음 보통 낮음

데이터 흐름 테스트는 데이터 결함에 강점이 있지만, 분석 정밀도를 높일수록 경로와 에일리어싱 처리 비용이 커진다. 전역 에일리어싱과 인터프로시저 해상도를 높이면 분석 시간도 증가하므로, 대규모 서비스에서는 요약 기반 분석과 캐시를 함께 사용한다.

동적 언어는 정적 해석만으로 경로를 확정하기 어렵다. 이 경우 런타임 계측과 테인트 추적으로 보완한다. 테스트 데이터셋의 다양성도 필요하며, 커버리지 수치만으로 판단하기보다 결함 밀도와 변경 범위를 결합한 지표로 운영한다.

CI에 연결할 때의 운영 방식

처음부터 모든 모듈에 All-DU-Paths를 적용하기보다 All-Defs, All-Uses를 우선 적용하고 위험 모듈에만 All-DU-Paths를 적용한다. 이와 함께 루프 언롤링 한계와 경로 샘플링 정책을 정한다.

정적 분석은 PR 게이트에 두고, 동적 DU 커버리지는 야간 빌드에서 수집하는 방식으로 분리할 수 있다. 실패가 발생하면 영향 분석 보고서를 자동 생성한다. 이후에는 변경 라인 주변의 DU 체인을 먼저 검증하고, 히스토리상 결함이 자주 발생한 구간에는 가중치를 적용해 범위를 확장한다.

데이터 결함 검출률은 1530%p 향상을 기대할 수 있으며, 조직·도메인 특성에 따른 편차를 고려해야 한다. 정적 단계에서 조기에 발견하면 평균 수정 리드타임을 2040% 단축하고, 릴리스 후 장애 대비 비용을 60~80% 절감할 수 있다. 데이터 라인리지와 DU 커버리지 리포트는 규정 준수와 감사 대응의 근거 자료가 되며, 변경 영향 분석을 정량화해 승인 프로세스도 간소화할 수 있다.

직선 경로에서 DU 이상 찾기

전제조건: Python 3.11+, pytest 설치(pip install pytest). 직선 경로 기준의 단순 정적 DU 이상 탐지 예시이며, 분기·에일리어싱 한계가 존재한다.

# file: dataflow_check.py
import ast
from dataclasses import dataclass
from typing import List, Dict

@dataclass
class Anomaly:
    var: str
    kind: str  # "UD", "DD", "DU-dead"
    line: int
    detail: str

class SimpleDUChecker(ast.NodeVisitor):
    def __init__(self):
        self.defined: Dict[str, str] = {}     # var -> last_op ("def" or "use")
        self.used_since_def: Dict[str, bool] = {}
        self.anomalies: List[Anomaly] = []

    def visit_Assign(self, node: ast.Assign):
        # RHS uses first
        self.visit(node.value)
        # LHS defs
        for t in node.targets:
            if isinstance(t, ast.Name):
                v = t.id
                if self.defined.get(v) == "def" and not self.used_since_def.get(v, False):
                    self.anomalies.append(Anomaly(v, "DD", node.lineno, "redef without use"))
                self.defined[v] = "def"
                self.used_since_def[v] = False
        self.generic_visit(node)

    def visit_Name(self, node: ast.Name):
        if isinstance(node.ctx, ast.Load):
            v = node.id
            if v not in self.defined:
                self.anomalies.append(Anomaly(v, "UD", node.lineno, "use before def"))
            else:
                self.defined[v] = "use"
                self.used_since_def[v] = True

    def finalize(self):
        for v, last in self.defined.items():
            if last in ("def",) and not self.used_since_def.get(v, False):
                self.anomalies.append(Anomaly(v, "DU-dead", -1, "defined but never used"))
        return self.anomalies

def check_source(src: str) -> List[Anomaly]:
    tree = ast.parse(src)
    checker = SimpleDUChecker()
    checker.visit(tree)
    return checker.finalize()

if __name__ == "__main__":
    sample = """
x = 0
x = 1      # DD (no use between defs)
y = x + 2  # OK use of x
z = y + u  # UD (u undefined)
"""
    for a in check_source(sample):
        print(a)
# file: test_dataflow_check.py
from dataflow_check import check_source

def test_du_anomalies_detected():
    src = """
a = 10
a = 20        # DD
b = a + 1     # OK
c = b + d     # UD (d)
"""
    anomalies = check_source(src)
    kinds = sorted((a.kind, a.var) for a in anomalies)
    assert ("DD", "a") in kinds
    assert ("UD", "d") in kinds
    assert any(a.kind == "DU-dead" and a.var == "c" for a in anomalies)

실행은 pytest -q로 한다. CI에서는 실패 시 PR을 차단하고 소스·라인·종류를 포함한 리포트를 업로드한다.

데이터 흐름 테스트소프트웨어 테스트정적 분석커버리지결함 검출