너무 태양에 가까이 날았던 가정

1980년대 초, NASA는 오늘날까지 안전 필수 엔지니어링을 괴롭히는 질문에 직면했다: 아직 발견하지 못한 버그를 어떻게 견딜 수 있는가? 그들의 답은 N-version programming이었다. 같은 명세를 세 개의 독립적인 팀에 주고, 세 프로그램을 병렬로 실행한 뒤, 출력에 투표하는 것이다. 한 팀이 버그를 작성하면 나머지 두 팀이 이길 투표로 제압할 것이다.

이것은 수학적 상식처럼 들린다. 하지만 틀렸다.

1986년, John Knight와 Nancy Leveson은 NASA의 자금으로 대규모 실험 결과를 발표했다. 두 대학의 27명의 대학원생이 같은 탄도 미사일 요격기 명세를 독립적으로 구현했다. 각 프로그램은 개별적으로 매우 신뢰할 수 있었다. 여섯 버전은 전혀 실패하지 않았고, 스물세 버전은 100만 개의 무작위 생성 테스트 케이스 중 99.9% 이상을 통과했다.

하지만 여러 버전이 같은 입력에서 실패할 때, 그들은 통계적 독립성이 예측한 것보다 훨씬 더 자주 함께 실패했다. z-score는 100.55였다. 99% 신뢰 구간에서 독립성에 대한 null hypothesis는 산산조각 났다. NASA는 인간의 오류가 무작위라고 내기했다. Knight와 Leveson은 그것이 뭉쳐 있다는 것을 증명했다.

N-Version Programming이 실제로 약속하는 것

N-version programming 뒤의 논리는 하드웨어 중복에서 빌려왔다. 로켓에 세 개의 동일한 자이로스코프를 달고 하나가 드리프트하면, 나머지 두 개가 그 드리프트를 이긴다. 고장은 독립적인 제조 편차, 온도 구배, 진동 모드에 영향을 받는 물리적 객체이기 때문에 독립적이다.

소프트웨어는 다르다. 세 팀에게 같은 문제를 풀게 하면, 세 개의 독립적인 주사위 굴림을 얻는 것이 아니다. 같은 모호한 명세를 읽고, 같은 교과서에서 같은 알고리즘을 배우고, 같은 언어와 같은 표준 라이브러리로 코드를 작성하는 세 명의 인간을 얻는 것이다. 그들의 입력이 상관관계를 가지므로 그들의 오류도 상관관계를 가진다.

실제로 N-version 아키텍처는 이렇게 생겼다:

from typing import Callable, List, TypeVar

T = TypeVar('T')

def n_version_vote(
    implementations: List[Callable[[float], T]],
    input_value: float
) -> T:
    """Run N versions and return the majority output."""
    results = [impl(input_value) for impl in implementations]
    
    # Simple majority vote
    from collections import Counter
    counts = Counter(str(r) for r in results)
    most_common = counts.most_common(1)[0][0]
    
    # Return the actual value that matched the winning string
    for r in results:
        if str(r) == most_common:
            return r
    
    raise RuntimeError("No consensus")

이 voter는 쉬운 부분이다. 어려운 부분은 그 안에 숨어 있는 가정이다: implementations[0], implementations[1], implementations[2]가 입력 공간의 상관관계가 없는 부분 집합에서 실패한다는 것. Knight와 Leveson은 그 가정이 바로 결함임을 보여주었다.

상관관계를 가진 실패가 어디서 오는가

Knight-Leveson 실험은 공통 모드 실패를 위한 두 가지 뚜렷한 메커니즘을 밝혀냈고, 둘 다 팀에게 “더 열심히 합시다”라고 부탁함으로써 고칠 수 있는 것이 아니었다.

첫 번째는 specification ambiguity이다. 미사일 요격기 명세에는 진정으로 어려운 엣지 케이스가 포함되어 있었다. 27명의 프로그래머 중 8명이 세 개의 레이더 점이 공선인 경우를 잘못 처리했다. 오류들은 각기 달랐다. 한 명은 배열 첨자에서 off-by-one을 저질렀고, 한 명은 수치 안정성이 낮은 알고리즘을 사용했고, 한 명은 경계 케이스를 완전히 잊었다. 하지만 문제 자체가 어려웠기 때문에, 프로그래머들이 부주의해서가 아니라 실패는 같은 어려운 입력에 집중되었다.

이것이 “difficulty factor”이다. 입력이 경계 조건에 위치하거나, 까다로운 부동소수점 연산을 필요로 하거나, 명세가 불충분한 상태 전환을 포함할 때, 독립적인 팀은 같은 곳에서 어려움을 겪는 경향이 있다. 그들의 해결책은 다르지만, 실패 영역은 겹친다.

두 번째 메커니즘은 shared mental models이다. 같은 커리큘럼에서 훈련받은 프로그래머들은 같은 휴리스틱을 적용한다. 같은 정렬 알고리즘에, 같은 방어적 복사 패턴에, 부동소수점 동등 비교를 위한 같은 epsilon에 손을 뻗는다. 그 공유된 기본값이 당면한 문제에 맞지 않을 때, 모든 팀이 같은 절벽으로 뛰어내린다.

NASA의 자체 Software Safety Guidebook은 결국 이것을 명시적으로 인정했다. 이 가이드는 “많은 전문가들이 N-Version programming을 비효율적이거나 심지어 역효과적이라고 여긴다”고 적고 있다. 실험용 항공기에 대한 NASA 연구 하나에서, 테스트 중 발견된 모든 소프트웨어 문제는 redundancy management system에서 왔다. 기본 비행 제어 소프트웨어는 완벽했다. N-version 계층만이 고장난 것이었다.

아무도 이야기하지 않는 복잡성 세금

N-version programming이 광고한 대로 작동했더라도, 그것은 무거운 대가를 요구한다. 세 가지의 완전한 구현 비용을 지불한다. 자체 로직을 포함하는 voter 비용을 지불한다. 세 프로세스를 실행하고 출력을 조정하는 운영 오버헤드 비용을 지불한다.

그 voter는 사소한 코드 조각이 아니다. 동점, 시간 초과, 다른 출력 형식, 그리고 다수가 틀린 경우까지 처리해야 한다. Brunelle과 Eckhardt는 1985년 SIFT operating system에서 이것을 입증했다. 두 개의 새로운 N-version이 원래 올바른 구현을 이기고 잘못된 답을 만들어냈다. 중복 시스템이 예방해야 할 오류를 스스로 만들어낸 것이다.

복잡성은 물어뜯기 전까지 측정하기 어려운 방식으로 가중된다. 이제 버전 관리를 할 배포 가능한 것이 세 개 있고, 유지보수할 테스트 매트릭스가 세 개 있으며, 요구사항이 필연적으로 변경될 때 명세 변경을 다르게 해석할 세 팀이 있다. 인간 오류를 위한 표면적 영역은 신뢰성 개선 속도보다 더 빨리 커진다.

대신 실제로 통하는 것

NASA는 fault tolerance를 포기한 것이 아니다. 그들은 독립성에서 다양성이 온다는 특정 가정을 포기했다. 현대의 고신뢰 시스템은 Knight와 Leveson이 식별한 실제 고장 모드를 다루는 기술들의 조합을 사용한다.

핵심 경로를 위한 formal methods. 세 팀이 명세를 올바르게 해석하기를 바라는 대신, 명세를 기계로 검증 가능한 형태로 작성한다. TLA+, Coq, SPIN과 같은 도구는 누구도 구현 코드 한 줄을 작성하기 전에 설계가 불변 조건을 만족하는지 검증한다. NASA의 자체 Remote Agent Experiment는 SPIN을 사용해 광범위한 테스트를 통과한 동시성 버그를 찾아냈다.

dissimilar technology stacks를 통한 다양성. 중복이 필요하다면 깊이 있게 하라. Airbus A330 비행 제어 시스템은 기본 채널과 백업 채널에 대해 다른 하드웨어 아키텍처, 다른 프로그래밍 언어, 독립적인 컴파일러를 사용한다. 목표는 단순히 독립적인 팀이 아니라 스택의 모든 계층에서 독립적인 고장 모드이다.

복제보다 단순화. NASA Software Safety Guidebook은 궁극적으로 N-version programming을 “작고 단순한 함수”에 대해서만 권장한다. 교훈은 화려하지 않지만 효과적이다: 직접 추론할 수 있을 만큼 단순한 시스템이 가장 신뢰할 수 있는 시스템이다. 중복 관리 코드의 모든 줄은 실패할 수 있는 줄이다.

다음은 실제로 단순화되고 명세 중심의 접근 방식이 어떻게 보이는지이다. 세 개의 불투명한 구현이 출력에 투표하는 대신, 핵심 불변 조건을 명시적으로 인코딩하고 런타임에 검증한다:

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class LaunchCommand:
    thrust_level: float  # 0.0 to 1.0
    abort_flag: bool
    
    def is_valid(self) -> bool:
        """Runtime invariant check derived from the formal spec."""
        if not (0.0 <= self.thrust_level <= 1.0):
            return False
        if self.abort_flag and self.thrust_level > 0.0:
            return False
        return True

def execute_command(cmd: LaunchCommand) -> Optional[str]:
    if not cmd.is_valid():
        raise ValueError("Invariant violation: command violates safety spec")
    
    # Execute only after the single, explicit check passes.
    return f"Executing thrust={cmd.thrust_level}, abort={cmd.abort_flag}"

이것은 N-version programming이 아니다. 명시적이고 테스트 가능한 하나의 안전 경계를 가진 하나의 구현이다. 경계가 노력이 들어가는 곳이다. 세 팀 중 두 팀이 맞았기를 바라는 데 노력을 쏟는 곳이 아니다.

현대의 메아리

Knight와 Leveson의 1986년 논문은 매년 더 관련성이 커지는 경고를 담고 있다. 상관관계를 가진 실패는 상관관계를 가진 팀을 필요로 하지 않는다. 상관관계를 가진 입력과 상관관계를 가진 어려움만 필요하다. AI 기반 코딩 도구가 확산됨에 따라, 우리는 행성 규모에서 새로운 N-version 실험을 실행하고 있다. 겹치는 말뭉치에 훈련되고 유사한 패턴으로 프롬프트된 모델은 공유된 고장 모드를 가진 코드를 생성한다.

LLM 코드 생성에 대한 최근 연구는 AI 생성 컴포넌트에 대해 15%에서 30% 사이의 co-error rate를 시사한다. beta factor, 즉 공통 원인에 기인한 실패의 비율은 이미 Knight와 Leveson이 측정한 인간 프로그래머 값을 초과할 수 있다. 우리는 더 많은 참가자와 똑같이 결함 있는 가정으로 실험을 반복하고 있다.

NASA는 이 교훈을 값비싼 방식으로 배웠다. 세 개의 구현이 필요한 것이 아니다. 명시하고, 검증하고, 신뢰할 수 있는 불변 조건을 가진 하나의 구현이 필요하다. 나머지는 공학으로 위장한 낙관주의에 불과하다.

자주 묻는 질문

N-version programming이란 무엇인가?

N-version programming은 여러 독립적인 팀이 같은 명세를 구현하는 소프트웨어 fault tolerance 기술이다. 프로그램들은 병렬로 실행되고 voter가 다수의 출력을 선택하며, 독립적인 팀이 독립적인 실수를 할 것이라는 가정에 기반한다.

NASA가 실제로 N-version programming을 사용했는가?

그렇다. NASA는 다중 버전 소프트웨어에 대한 초기 연구를 자금 지원했고, 한때 일부 정책 문서가 fault-tolerant 시스템에 대해 N-version programming을 사실상 의무화했다. 스페이스 셔틀의 엔진 시동 시퀀스에서 이것을 사용했지만, 경험적 연구가 한계를 드러낸 후 NASA는 이를 작고 단순한 함수로 사용을 제한했다.

Knight-Leveson 실험이란 무엇인가?

1986년, John Knight와 Nancy Leveson은 27명의 프로그래머에게 미사일 요격기 명세를 독립적으로 구현하게 했다. 100만 개 이상의 테스트에서, 프로그램들은 통계적 독립성이 예측할 것보다 훨씬 더 많은 동시 실패를 보였고, N-version programming의 핵심 가정을 기각했다.

N-version programming을 사용할 가치가 있는 경우가 있는가?

제한된 경우에 그렇다. NASA의 현재 지침에 따르면, 진정한 다양성이 강제될 수 있는 작고 잘 정의된 함수에 적합할 수 있다. 일반적인 애플리케이션 개발에서는 복잡성 비용과 상관관계를 가진 실패 위험이 이점을 상회한다. Formal methods, runtime invariant checking, 단순화가 보통 더 나은 투자이다.