같은 버그를 두 번 고치는 것은 프로세스 실패다
모든 팀에는 계속 돌아오는 결함이 하나씩 있다. 페이지네이션의 오프바이원. 인증 미들웨어의 누락된 null 체크. 세 스프린트 전에 누군가 “고쳤다”는 체크아웃의 race condition.
같은 버그를 세 번 쓴 것이 아니다. 같은 원인을 가진 세 가지 다른 버그를 쓴 것이다. 수정은 증상을 다뤘다. 원인은 여전히 숨어 있었다.
Fagan inspection은 결함이 빠져나가기 전에 발견하도록 설계되었다. 대부분의 팀은 재작업 단계에서 멈춘다. 작성자가 기록된 문제를 수정하고, 중재자가 수정을 검증하고, 모두가 넘어간다. 그것은 실수다. 후속 단계가 인과 분석이 속한 곳이다. 이를 건너뛰면 같은 결함 유형을 위한 다음 검사를 예약하는 셈이다.
인과 분석이 실제로 의미하는 것
인과 분석은 근본 원인 분석이 아니다. 근본 원인 분석은 “어떤 코드 줄이 실패했고 왜 그런가”를 묻는다. 인과 분석은 “우리 프로세스의 어떤 부분이 이 결함 범주가 존재하도록 허용했는가”를 묻는다.
null 포인터 예외의 근본 원인은 “null 체크를 잊었다”다. 인과 분석의 발견은 “우리의 리뷰 체크리스트에 null 안전성이 포함되어 있지 않고, 린팅 규칙이 확인되지 않은 역참조를 허용한다”다. 하나는 버그를 고친다. 다른 하나는 공장을 고친다.
Fagan inspection에서 인과 분석은 재작업 후에 이루어진다. 중재자가 결함을 범주별로 그룹화하고 프로세스 수준의 원인을 파악하는 짧은 세션을 주도한다. 산출물은 코드 변경이 아니다. 프로세스 변경이다. 업데이트된 체크리스트, 새로운 린트 규칙, 교육 격차, 또는 수정된 진입 기준.
작동 방식: 실행 방법
검사 로그부터 시작하라. 각 결함에는 이미 네 가지 필드가 있어야 한다. 위치, 심각도, 유형, 설명. 인과 분석 중에 다섯 번째를 추가하라. 프로세스 원인.
중재자가 결함을 유형별로 그룹화한다. 열두 개의 결함 중 세 개가 경계 조건 오류라면 그것은 패턴이다. 두 개가 같은 모듈의 API 오용 오류라면 그것도 패턴이다. 패턴이 신호다. 개별 결함은 소음이다.
각 패턴에 대해 세 가지 질문을 하라.
- 검사 전에 이 범주를 예방할 수 있었는가?
- 기존의 예방 메커니즘이 왜 이것을 놓쳤는가?
- 다음에 이 범주를 예방할 수 있는 가장 저렴한 변경은 무엇인가?
세 번째 질문에서 대부분의 팀이 잘못된다. 그들은 아키텍처를 다시 작성하자고 제안한다. 그것은 희망적 사고다. 목표는 그 범주를 제거하는 가장 작은 프로세스 변경이다.
경계 조건 패턴은 리뷰 체크리스트에 “오프바이원 및 경계 체크”를 추가하는 것을 의미할 수 있다. API 오용 패턴은 정적 분석 규칙을 추가하는 것을 의미할 수 있다. 누락된 오류 처리 패턴은 오류 경로 테스트를 요구하도록 완료 기준을 업데이트하는 것을 의미할 수 있다.
작동하는 인과 분석 추적기
다음은 검사 로그를 가져와 결함을 유형별로 그룹화하고 프로세스 수준의 원인을 묻는 Python 스크립트다.
#!/usr/bin/env python3
"""
Run causal analysis on a Fagan inspection log.
Reads a JSON log, groups defects by type, and emits a causal analysis report.
"""
import json
import argparse
from collections import defaultdict
from dataclasses import dataclass, field
from typing import List, Dict
@dataclass
class Defect:
location: str
severity: str
defect_type: str
description: str
@dataclass
class CausalPattern:
defect_type: str
count: int
locations: List[str] = field(default_factory=list)
process_cause: str = ""
proposed_fix: str = ""
def load_log(path: str) -> List[Defect]:
with open(path, "r") as f:
raw = json.load(f)
return [Defect(**item) for item in raw]
def analyze_patterns(defects: List[Defect]) -> List[CausalPattern]:
groups: Dict[str, List[Defect]] = defaultdict(list)
for d in defects:
groups[d.defect_type].append(d)
patterns = []
for dtype, items in groups.items():
patterns.append(CausalPattern(
defect_type=dtype,
count=len(items),
locations=[d.location for d in items],
))
return sorted(patterns, key=lambda p: p.count, reverse=True)
def prompt_causal_input(patterns: List[CausalPattern]) -> List[CausalPattern]:
print("=== CAUSAL ANALYSIS SESSION ===")
print("For each pattern, identify the process cause and the cheapest fix.\n")
for p in patterns:
print(f"Pattern: {p.defect_type} ({p.count} occurrence(s))")
print(f"Locations: {', '.join(p.locations)}")
p.process_cause = input("Process cause: ").strip()
p.proposed_fix = input("Cheapest prevention fix: ").strip()
print()
return patterns
def emit_report(patterns: List[CausalPattern], output_path: str):
report = {
"summary": {
"total_patterns": len(patterns),
"total_defects": sum(p.count for p in patterns),
},
"patterns": [
{
"type": p.defect_type,
"count": p.count,
"locations": p.locations,
"process_cause": p.process_cause,
"proposed_fix": p.proposed_fix,
}
for p in patterns
],
}
with open(output_path, "w") as f:
json.dump(report, f, indent=2)
print(f"Report written to {output_path}")
def main():
parser = argparse.ArgumentParser(description="Causal analysis for Fagan inspections")
parser.add_argument("log", help="Path to inspection log JSON")
parser.add_argument("--output", default="causal_report.json", help="Output report path")
args = parser.parse_args()
defects = load_log(args.log)
patterns = analyze_patterns(defects)
if not patterns:
print("No defects found. Nothing to analyze.")
return
patterns = prompt_causal_input(patterns)
emit_report(patterns, args.output)
if __name__ == "__main__":
main()
검사 로그를 inspection_log.json으로 저장하라.
[
{"location": "src/auth.py:42", "severity": "major", "defect_type": "null-safety", "description": "Missing null check on user object"},
{"location": "src/orders.py:88", "severity": "minor", "defect_type": "boundary", "description": "Off-by-one in pagination limit"},
{"location": "src/auth.py:67", "severity": "major", "defect_type": "null-safety", "description": "Unchecked token decode result"}
]
python causal_analysis.py inspection_log.json를 실행하면 스크립트가 프로세스 원인을 식별하는 과정을 안내한다. 생성된 보고서는 다음 회고나 프로세스 개선 주기의 입력이 된다.
왜 대부분의 팀이 이것을 건너뛰는가
인과 분석은 이미 비싼 프로세스에 시간을 추가한다. 250줄의 표준 Fagan inspection에는 812인시가 소요된다. 인과 분석은 추가로 3060분을 더한다.
백로그가 있을 때 이 추가 시간은 낭비처럼 느껴진다. 하지만 인과 분석이 결함 범주의 재발을 한 번이라도 막는다면, 그 범주가 다음에 나타나지 않을 때 스스로 비용을 회수한다.
더 어려운 문제는 정직함이다. 인과 분석은 종종 결함이 존재한 이유가 팀이 한 단계를 건너뛰었기 때문임을 밝혀낸다. 코드는 검사 전에 테스트되지 않았다. 리뷰어는 체크리스트를 사용하지 않았다. 체크리스트 자체가 불완전하다.
이러한 발견은 불편할 수 있다. 인과 분석을 책임 소재 규정으로 다루는 팀은 더 이상 정직한 답변을 얻지 못할 것이다. 중재자는 이를 책임 극단주의가 아닌 프로세스 개선으로 프레이밍해야 한다.
진정한 트레이드오프: 속도 대 학습
Fagan inspection 후에는 두 가지 선택지가 있다. 수정을 검증하고 넘어감으로써 고리를 닫는다. 또는 수정을 검증하고 결함이 왜 존재했는지 배움으로써 고리를 닫는다.
첫 번째 선택지는 오늘 더 빠르다. 두 번째 선택지는 향후 6개월 동안 더 빠르다.
Fagan inspection에서 가장 큰 가치를 얻는 팀은 검사 로그를 데이터셋으로 다룬다. 결함 패턴은 피드백이다. 이를 무시하는 것은 테스트 스위트를 실행하고 실패를 보지 않는 것과 같다.
모든 결함 범주가 인과 분석을 받을 자격이 있는 것은 아니다. 한 번뿐인 오타에는 프로세스 변경이 필요 없다. 하지만 어떤 범주가 두 번 연속 검사에 나타난다면, 코드 문제로 위장한 프로세스 문제가 있는 것이다.
영향이 가장 큰 모듈부터 시작하라
모든 검사에서 인과 분석을 실행할 필요는 없다. 프로덕션 사고나 유출된 결함이 가장 많이 발생하는 모듈을 고른다. 전체 Fagan inspection을 실행하고, 로그를 수집하고, 인과 분석에 30분을 할애한다.
처음 할 때는 제안된 수정이 명백할 것이다. 체크리스트를 업데이트하라. 린트 규칙을 추가하라. 패턴에 대한 짧은 팀 노트를 작성하라. 세 번째 검사까지는 이미 분석한 범주에서 결함이 줄어 있어야 한다.
수치가 떨어지지 않는다면 제안된 수정이 너무 모호한 것이다. “더 조심하라”는 프로세스 변경이 아니다. “CI에서 null safety linter를 실행하라”는 그렇다.
시간이 지남에 따라 검사별 결함 범주를 추적하라. 인과 분석이 제 역할을 한다면 반복되는 범주는 사라진다. 사라지지 않는다면 원인을 잘못 파악했거나 수정을 제대로 실행하지 못한 것이다.
FAQ
Fagan inspection에서 인과 분석이란 무엇인가?
인과 분석은 검사 후 단계로, 팀이 발견된 결함을 조사하고 범주별로 그룹화하며 프로세스 수준의 원인을 식별하는 것이다. 목표는 개별 결함을 수정하는 것만이 아니라 체크리스트, 도구, 관행을 변경하여 재발을 방지하는 것이다.
인과 분석과 근본 원인 분석의 차이는 무엇인가?
근본 원인 분석은 누락된 null 체크와 같이 단일 결함이 발생한 구체적인 이유를 식별한다. 인과 분석은 누락된 체크리스트 항목이나 강제되지 않은 린트 규칙과 같이, 프로세스가 해당 결함 범주 전체가 작성되도록 허용한 이유를 식별한다.
인과 분석은 언제 실행해야 하는가?
Fagan inspection의 재작업 및 후속 단계 후에 결함이 여전히 생생할 때 실행하라. 여러 결함에 나타나거나 이전 검사에 나타난 패턴에 집중하라. 한 번뿐인 결함은 거의 프로세스 변경을 정당화하지 못한다.
인과 분석이 작동하는지 어떻게 아는가?
시간이 지남에 따라 검사 전반에 걸쳐 결함 범주를 추적하라. 인과 분석이 효과적이라면 반복되는 범주의 빈도가 떨어져야 한다. 같은 범주가 계속 나타난다면 제안된 수정이 잘못되었거나 실행되지 않고 있는 것이다.
다음 검사 로그에서 실행하라
마지막 검사 로그를 찾아라. 결함을 유형별로 그룹화하라. 상위 범주에 대해 다음에 이를 예방할 수 있는 가장 저렴한 프로세스 변경이 무엇인지 물어라.
그 변경을 적어라. 담당자를 배정하라. 다음 검사에서 검증하라. 그것이 인과 분석이다. 나머지는 그냥 버그 고치기다.