두 명의 리뷰어, 하나의 diff, 제로 중복

두 명의 시니어 엔지니어가 동일한 풀 리퀘스트를 리뷰한다. 한 명은 누락된 null 체크를 지적한다. 다른 한 명은 클린업 패스의 레이스 컨디션을 발견한다. 둘 다 둘을 모두 찾지는 못한다.

리뷰어를 한 명만 배정했다면, 그 버그 중 하나는 출시되었을 것이다. 이것은 기술 격차가 아니다. 인간의 주의력의 예측 가능한 특성이며, Michael Fagan이 1976년 IBM에서 기록한 것이다.

Fagan은 IBM의 소프트웨어 파이프라인에서 결함 탐지율을 측정하고 있었다. 그의 데이터는 불편한 사실을 보여주었다. 심지어 경험 많은 검사자도 그룹이 최종적으로 발견한 전체 결함의 극히 일부만 잡아냈다. 진정한 가치는 어느 한 개인의 전문성에 있지 않았다. 여러 관점을 구조화하여 조합하는 데 있었다.

오늘날 대부분의 팀은 그 조합을 구조화하지 않는다. 시니어 엔지니어가 회의 사이에 diff를 훑어보고 스타일 이슈를 알아챈 뒤 승인하고 지나간다. 다음 리뷰어도 똑같이 한다. 둘 다 다음 주 화요일에 프로덕션 데이터를 손상시킬 오프바이원 에러를 놓친다.

Fagan은 이를 비구조화 리뷰 증후군이라 불렀다. 그룹에는 눈이 있지만 프로세스는 없다.

Fagan 인스펙션이 실제로 무엇인가

Fagan 인스펙션은 사람들이 함께 코드를 읽고 감정을 나누는 회의가 아니다. 정의된 역할, 진입 기준, 측정 가능한 산출물을 갖춘 공식적인 프로세스다. Fagan은 비구조화된 리뷰가 시간을 낭비하고 리뷰를 전혀 하지 않는 것과 거의 동일한 비율로 결함을 누출시킨다는 것을 알고 이를 설계했다.

핵심 통찰은 역할 분리다. 각자는 정확히 하나의 일을 맡는다:

  • 중재자는 회의를 진행하고 규칙을 집행한다. 검사하지는 않는다.
  • 리더는 코드를 소리 내어 다른 말로 옮긴다. 이는 그룹이 작성자가 의도한 것이 아니라 코드가 실제로 하는 일에 직면하도록 강제한다.
  • 테스터는 실행 경로, 경계 조건, 커버리지 공백에 대해 생각한다.
  • 작성자는 질문에 답하지만 코드를 옹호하지는 않는다.

이 분리는 그룹 리뷰에서 가장 흔한 실패 모드, 즉 작성자가 모두를 자신의 우려에서 설득해내는 것을 방지한다.

작성자가 설명자이기도 할 때, 그들은 모호함을 다듬는다. “아, 그 변수는 항상 호출자에 의해 설정돼.” 그룹이 끄덕인다. 아무도 확인하지 않는다. 리더 역할은 이 습관을 깨기 위해 존재한다. 리더가 한 문장으로 함수를 다른 말로 옮길 수 없다면, 그 함수는 출시할 준비가 되지 않은 것이다.

왜 체크리스트가 직관을 이기는가

Fagan은 검사 체크리스트도 도입했다. 이는 스타일 가이드에서 복사한 일반적인 코딩 표준이 아니다. 리뷰 대상인 특정 유형의 산출물에 맞게 조정되어 있다.

상태 머신을 위한 체크리스트는 묻는다: 모든 전이를 처리했는가? 리소스 할당자를 위한 체크리스트는 묻는다: 모든 경로에서 모든 할당에 해제가 짝을 이루는가?

체크리스트가 존재하는 이유는 인간의 주의력이 불규칙하기 때문이다. 만 개의 데이터베이스 쿼리를 작성한 전문가라도 BEGIN TRANSACTION 블록을 정신적으로 건너뛴다. 그들의 뇌는 그것을 올바른 것으로 자동 완성한다. Fagan은 체크리스트에 의해 주도되는 검사자가 직관에 의해 주도되는 리뷰어가 지나친 결함을 발견했다는 것을 밝혀냈다. 전문가들이 부주의해서가 아니라, 전문성이 맹점을 만들기 때문이다.

다음은 단일 Python 함수를 위한 경량 체크리스트다:

CHECKLIST = [
    "Can a non-author paraphrase what this function does in one sentence?",
    "Does every execution path return or raise predictably?",
    "What happens at the minimum and maximum valid inputs?",
    "What happens at exactly one step past the boundary?",
    "Does the function mutate any argument, closure, or global state?",
    "Is every resource acquired also released on the error path?",
    "If this raises, can the caller distinguish recoverable from fatal?",
]

이것은 관료주의가 아니다. 체계적 주의를 강제하는 강제 기능이다.

Fagan은 인스펙션을 네 단계로 나눴다. 회의는 가장 짧은 단계다

진정한 Fagan 인스펙션에는 네 단계가 있으며, 회의 자체는 가장 짧은 단계다.

준비. 모든 검사자는 그룹이 만나기 전에 체크리스트를 가지고 자료를 혼자 검토한다. Fagan은 준비를 한 검사자가 준비 없이 들어온 검사자보다 약 두 배의 결함을 발견했다는 것을 밝혀냈다. 회의는 결함을 생성하는 것이 아니라 발견을 조합하기 위해서만 존재한다.

회의. 리더가 코드를 훑어본다. 테스터가 만약에 대한 질문을 한다. 중재자는 2시간 이내로 끝낸다. 작성자는 메모를 한다. 회의 중에 아무도 코드를 수정하지 않는다. 결함은 기록되고 그룹은 다음으로 넘어간다.

재작업. 작성자는 기록된 결함을 혼자 수정한다.

후속 조치. 중재자는 모든 결함이 처리되었는지 확인한다. 대규모 재작업은 두 번째 인스펙션을 유발할 수 있다.

이 구조는 현대의 풀 리퀘스트에는 무겁게 느껴진다. Fagan은 단일 결함이 수백만 달러의 비용을 초래할 수 있는 메인프레임 소프트웨어를 위해 이를 설계했다. 하지만 원칙은 더 작은 맥락에도 여전히 적용 가능하다.

과잉이 되는 지점

Fagan 인스펙션은 공짜가 아니다. 준비 시간만으로도 상당한 오버헤드가 추가된다. 열 줄짜리 버그 수정에 대해 완전한 Fagan 인스펙션은 터무니없다. 누락된 임포트를 찾는 데 네 명과 체크리스트가 필요하지 않다.

페이오프 곡선은 비선형적이다. Fagan의 데이터는 인스펙션이 가장 효과적인 것은 복잡하고 고위험 모듈, 즉 상태 머신, 파서, 리소스 매니저, 비로컬 상태나 미묘한 순서 제약이 있는 모든 것이라고 제안했다. CRUD 핸들러와 보일러플레이트 테스트에는 비공식 리뷰로 충분하다.

진정한 실수는 모든 변경에 동일한 리뷰 전략을 적용하는 것이다. 로그 메시지의 오타에는 Fagan 인스펙션이 필요 없다. 분산 트랜잭션 코디네이터에는 아마도 필요하다.

오늘 사용할 수 있는 경량 버전

IBM의 회의 문화가 없어도 대부분의 이점을 얻을 수 있다. 다음은 현대 팀에 적용 가능한 경량 적응판이다:

  1. 그룹 논의 전에 개별 리뷰를 의무화하라. 모든 리뷰어는 누군가 말하기 전에 서면 코멘트를 제출한다. 이는 첫 번째 큰소리 의견이 지배하는 것을 방지한다.

  2. 리더 역할을 순환시켜라. 누군가 비판하기 전에 한 리뷰어에게 변경 사항을 자신의 말로 요약해달라고 요청하라. 할 수 없다면, 변경 사항이 너무 크거나 불분명한 것이다.

  3. 팀 체크리스트를 구축하라. 위의 일곱 가지 질문으로 시작하라. 도메인별 항목을 추가하라. 분기별로 재검토하라.

  4. 작성자와 옹호자를 분리하라. 작성자는 사실에 기반한 질문에 답한다. 코드가 괜찮다고 주장하지 않는다. 리뷰어가 혼란스러워한다면, 그것은 논쟁이 아니라 데이터다.

  5. 결함을 기록하고, 나중에 수정하라. 리뷰 중에 코드를 고치지 마라. 이슈를 기록하고, 끝낸 뒤 수정하라.

다음은 임의의 Python 모듈에 대한 리뷰 체크리스트를 생성하는 간단한 스크립트다:

import ast
import sys
from pathlib import Path

def generate_checklist(source_path: str) -> list[str]:
    """Generate a Fagan-style checklist from a Python module."""
    source = Path(source_path).read_text()
    tree = ast.parse(source)

    checklist = [
        f"Module {Path(source_path).name}: {len(tree.body)} top-level statements",
        "Can a non-author state the module's responsibility in one sentence?",
    ]

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            checklist.append(
                f"Function '{node.name}': does every path return or raise?"
            )
            if any(isinstance(n, ast.Try) for n in ast.walk(node)):
                checklist.append(
                    f"Function '{node.name}': is every exception handled explicitly?"
                )

    return checklist

if __name__ == "__main__":
    for item in generate_checklist(sys.argv[1]):
        print(f"[ ] {item}")

다음과 같이 실행하라:

python inspection_checklist.py src/transaction.py

이것은 버그를 찾아주지 않는다. 올바른 장소를 보도록 강제한다.

더 똑똑한 사람을 고용해도 프로세스 문제는 고쳐지지 않는다

Fagan의 연구는 50년 가까이 되었지만, 그 발견은 변하지 않았다. 개인 리뷰어는 일관성이 없다. 그룹 역시 구조화되지 않으면 일관성이 없다. 리뷰어 간의 분산은 제거해야 할 문제가 아니다. 조직화해야 할 자원이다.

다음에 한 리뷰어가 다른 사람이 놓친 버그를 발견했을 때, 누가 더 나은지 묻지 마라. 프로세스가 둘 다 보고 있는 것을 조합하기에 충분히 구조화되어 있는지 물어라. Fagan은 이미 고용으로 이 문제를 해결하려 시도했다. 효과가 없다.

FAQ

Fagan 인스펙션이란 무엇인가?

Fagan 인스펙션은 1976년 IBM의 Michael Fagan이 개발한 공식적이고 구조화된 코드 리뷰 프로세스다. 정의된 역할(중재자, 리더, 테스터, 작성자), 준비 요건, 체크리스트를 사용하여 소프트웨어 산출물의 결함 탐지를 극대화한다.

왜 다른 리뷰어가 다른 버그를 찾는가?

인간의 주의력은 선택적이다. 전문가들은 코드를 빠르게 읽을 수 있는 정신적 지름길을 발달시키지만, 그 같은 지름길이 맹점을 만든다. 다른 리뷰어는 다른 배경과 인지 패턴을 가지므로, 그들의 맹점은 완벽하게 겹치지 않는다. Fagan의 연구는 인스펙션의 가치가 완벽한 리뷰어를 찾는 것이 아니라, 여러 불완전한 관점을 조합하는 데 있다는 것을 보여주었다.

Fagan 인스펙션은 오늘날에도 사용되는가?

완전한 공식 프로세스는 항공우주와 의료기기와 같은 안전이 중요한 산업 외에서는 드물다. 그러나 개별 준비, 역할 분리, 체크리스트 주도 리뷰라는 근본 원칙은 고성능 소프트웨어 팀에서 점점 더 채택되고 있다. 핵심 아이디어는 구조화된 워크스루와 공식 기술 리뷰와 같은 현대적 실천에도 영향을 미쳤다.

완전한 Fagan 인스펙션의 오버헤드가 언제 가치가 있는가?

결함이 심각한 결과를 초래하는 모듈에서: 분산 합의 로직, 보안 경계, 리소스 라이프사이클, 상태 머신. 일상적인 변경에는 경량 적응판으로 보통 충분하다. 모든 diff에 동일한 프로세스를 적용하는 것이 아니라, 실제 위험에 맞춰 리뷰의 엄격함을 조정하라.