IBM의 Cleanroom software engineering process는 천 줄당 0.1개의 defect를 달성했다. 당시 업계 평균은 10에서 50 사이였다. 이 process는 문서화되었고, 여러 프로젝트와 언어에 걸쳐 replicate되었으며, 독립적으로 검증되었다. 그리고 사라졌다.
더 나은 method가 이를 대체했기 때문이 아니다. Zero-defect engineering이 사라진 것은, 이를 가치 있게 만들던 경제적 조건이 변했고, 이를 가능하게 했던 조건이 소수의 government contractor를 넘어 확산되지 않았기 때문이다.
문제: defect는 처음부터 설계되었다
대부분의 software는 bug가 불가피하다는 암묵적인 가정 위에 구축된다. code를 작성하고, 실행하고, 문제를 찾고, 수정한다. 이 loop는 생산적으로 느껴진다. 이는 또한 당신의 process가 defect를 정상적인 output으로 생산한다는 양보도 된다.
Cleanroom은 이 가정을 완전히 거부했다. 1970년대 IBM의 Harlan Mills에 의해 개발된 이 method는 software를 반도체 제조처럼 다루었다. chip을 제조한 뒤 품질을 테스트해 넣는 것이 아니다. defect를 만들어내는 조건을 처음부터 방지하는 것이다.
이 method에는 세 가지 상호 연결된 practice가 있었다. Statistical process control 하의 incremental development. Black-box, state-box, clear-box 구조를 사용하는 function-theoretic design. 그리고 가장 많은 머리를 아프게 한 rule: code를 작성한 사람들은 그 code를 실행하는 것이 금지되었다.
이는 sadism이 아니었다. Forcing function이었다. code를 실행하여 작동하는지 확인할 수 없다면, 타이핑하기 전에 correctness에 대해 추론해야 한다.
숫자는 우연이 아니었다
IBM Federal Systems Division은 NASA satellite control system에 Cleanroom을 적용하여 최종 테스트에서 KLOC당 0.1개의 defect를 측정했다. COBOL billing system은 0.3을 달성했다. Ada real-time system은 0.4를 달성했다. 결과는 팀, 응용 도메인, 프로그래밍 언어를 가로질러 유지되었다.
이 process는 일정도 압축했다. 당시 업계 데이터는 software effort의 50~70퍼센트가 testing과 debugging에 소비되었음을 보여주었다. Cleanroom 팀은 그 시간을 대신 design에 소비했다. code는 처음 컴파일될 때 작동했다.
왜 작동하던 process가 버려졌는가
만약 이 method가 이토록 효과적이었다면, 왜 사라졌는가?
짧은 답은 1990년에서 2010년 사이에 software economics가 뒤집혔기 때문이다. 긴 답은 zero-defect engineering은 더 이상 존재하지 않는 세계에 최적화되어 있었다는 것이다.
1970년대와 1980년대, software는 물리적 매체로 배송되었다. production의 bug는 recall, patch disk, 또는 현장 기술자를 필요로 했다. defect의 비용은 엄청났다. Zero-defect engineering은 비쌌지만, 하나의 recall을 피하는 것만으로도 전체 process를 상쇄했다.
Web은 cost function을 바꾸었다. 오늘날 우리는 over the wire로 deploy한다. bug가 production에 도달하면, 몇 분 만에 rollback하고, 몇 시간 만에 patch한다. 단일 defect의 비용은 수차례 감소했다. Zero-defect engineering의 비용은 전혀 감소하지 않았다. 여전히 formal verification, statistical process control, 그리고 개발자가 자신의 code를 실행하지 않아야 한다는 것이 필요하다. 이러한 practice들은 현대의 product development가 지출을 거부하는 시간을 소비한다.
상호 의존의 함정
Cleanroom은 메뉴가 아니다. 마음에 드는 부분만 채택할 수 없다.
Statistical process control은 모든 increment를 측정하고 defect target을 놓쳤을 때 중단하는 경우에만 작동한다. Box structures는 다음을 작성하기 전에 각 레벨을 검증하는 경우에만 작동한다. No-execution rule은 절대적일 때만 작동한다. “이것이 컴파일되는지 빨리 확인만 해보자”는 개발자는 forcing function을 파괴한다.
부분적인 채택은 어떤 benefit도 주지 않고 모든 overhead를 준다. 팀은 한 번의 sprint로 Cleanroom을 시도할 수 없다. 그들은 전체 사이클을 재구성하고, 모든 개발자를 재교육하며, 수 개월 동안 process data를 수집해야 한다. 이 proposal에 직면한 대부분의 manager는 대신 다른 QA engineer를 고용한다.
Velocity premium이 quality premium을 죽였다
현대의 software는 time-to-market에서 경쟁한다. consumer software에서 first mover advantage는 출시 후 bug를 수정하는 비용보다 더 가치 있다. 투자자들은 defect density metrics가 아닌 growth curves를 보상한다.
Zero-defect engineering은 다른 scoreboard를 위해 최적화된다. correctness가 주요 constraint이고 schedule pressure가 부차적이라고 가정한다. 이는 NASA satellite control system에 대해서는 사실이었다. 경쟁자보다 먼저 ship하려는 startup에 대해서는 사실이 아니다.
문화적 불일치는 더 깊다. 개발자들은 code를 실행하는 것을 좋아한다. write-run-fix의 immediate feedback loop는 만족스럽다. Cleanroom은 모든 edge case를 생각해낼 때까지 그 만족을 미루라고 요청한다.
업계는 다른 social contract를 선택했다. 우리는 speed를 대가로 bug를 참아내고, continuous feedback을 대가로 이를 patch한다. 이는 합리적인 trade이다. 이것이 또한 평균적인 web application의 defect rate가 IBM의 Cleanroom 수치보다 1980년대 평균에 더 가까운 이유다.
무엇과 바꾸었는가
대체재는 correctness보다 iteration을 우선시하는 일련의 practice였다.
Continuous integration은 bug 발견을 더 빠르게 했지만, code를 correct by construction으로 만들지는 않았다. Agile은 feedback loops를 단축했지만 design phases도 단축했다. Test-driven development는 여전히 Cleanroom이 거부한 write-test-fix loop를 전제로 한다.
이들 중 어느 것도 bad practice가 아니다. 현대의 stack은 사용자가 무엇을 원하는지 발견하는 것을 최적화한다. Zero-defect engineering은 알려진 specification을 올바르게 구현하는 것을 최적화했다.
전체 시스템을 채택하지 않고 훔칠 수 있는 것
당신의 회사에서 완전한 Cleanroom을 구현할 수는 없을 것이다. 하지만 underlying principles는 이전되며, 일부 현대의 tool은 해당 method의 rigour를 overhead 없이 근사한다.
Contracts를 작성하라, comments만이 아니라. Preconditions와 postconditions를 명시적으로 정의하고 code에서 enforce하라.
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Transfer:
from_balance: Decimal
to_balance: Decimal
amount: Decimal
def execute(self) -> tuple[Decimal, Decimal]:
# Preconditions stated and checked
assert self.amount > 0, "transfer amount must be positive"
assert self.from_balance >= self.amount, "insufficient funds"
new_from = self.from_balance - self.amount
new_to = self.to_balance + self.amount
# Postcondition: total value is conserved
assert new_from + new_to == self.from_balance + self.to_balance
return new_from, new_to
가정을 executable checks로 encode함으로써, 암묵적인 reasoning을 명시적인 guards로 변환하여 development 중에 edge cases를 생각하도록 강제한다.
Property-based testing을 statistical process control로 사용하라. Cleanroom은 usage profiles에 기반한 statistical testing을 사용했다. 현대의 property-based testing tool은 random inputs을 생성하고 invariants를 검증함으로써 유사한 일을 한다.
from hypothesis import given, strategies as st
from datetime import datetime, timedelta
@given(
start=st.datetimes(min_value=datetime(2000, 1, 1)),
delta=st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=365))
)
def test_duration_roundtrips(start, delta):
"""Adding then subtracting the same duration must return the original."""
assert start + delta - delta == start
이 test는 엄청난 input space에 걸쳐 mathematical property를 assert한다. 반례를 찾으면 minimal failing case로 shrink한다. 당신은 specific examples가 아닌 computation의 구조를 확인하고 있다.
Invalid states를 unrepresentable로 만들어라. Cleanroom의 box structure methodology는 증가하는 세부 수준에서 행동을 정의하는 것에 관한 것이었다. 현대의 동등물은 illegal states를 방지하기 위해 static types를 사용하는 것이다.
from typing import NewType
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def fetch_order(order_id: OrderId) -> dict:
...
# This will not compile in a typed codebase:
# fetch_order(UserId(42)) # type error: expected OrderId, got UserId
Type checker는 어떤 test보다도 먼저 실행되는 verification layer가 된다. UserId를 OrderId가 있어야 할 곳에 전달할 수 없다. 왜냐하면 type system이 그 실수를 구조적으로 불가능하게 만들기 때문이다.
여전히 중요한 곳
Zero-defect engineering이 틀렸게 된 것은 아니다. Niche가 된 것이다.
단 하나의 bug가 사람을 죽이는 safety-critical domain에서는 여전히 사용된다. Medical devices, avionics, nuclear control systems은 여전히 Cleanroom에서 파생된 process를 사용한다. 왜냐하면 defect의 비용은 여전히 천문학적이기 때문이다. FDA와 DO-178C standards는 다른 이름 아래 동일한 아이디어의 많은 부분을 보존한다.
우리 나머지에게 web application의 bug는 support ticket과 한 번의 deploy의 비용이다. Pacemaker의 bug는 한 명의 생명의 비용이다. Method가 작동을 멈춘 것은 아니다. 우리가 이를 필요로 하지 않게 된 것이다.
불편한 진실
Zero-defect engineering이 사라진 것은 실패했기 때문이 아니라, 업계가 success를 재정의했기 때문이다. 목표는 “처음부터 작동하는 software를 ship하는 것”에서 “망가진 version이 사용자가 알아차리기 전에 교체될 만큼 충분히 빨리 software를 ship하는 것”으로 이동했다.
이는 옹호할 수 있는 trade이다. 이는 현대의 internet을 구축했다. 이것은 또한 대부분의 software가 defect를 남기는 것을 수학적으로 보장하는 process로 구축되고, 우리가 이를 받아들이는 이유이기도 하다. 나중에 수정하는 것이 이제 충분히 저렴하기 때문이다.
Cleanroom을 채택하지 않아도 더 나은 code를 작성할 수 있다. 하나의 function부터 시작하라. 그 body보다 먼저 contract를 작성하라. Invariants에 대해 property-based tests를 실행하라. Types를 사용하여 illegal states를 도달 불가능하게 만들어라. Bugs가 어디에서 오는지 track하고 이를 생산하는 process를 고쳐라.
KLOC당 zero defects는 아마도 당신의 목표가 아닐 것이다. 하지만 이 method가 왜 작동했고 우리가 왜 신경 쓰기를 그만두었는지 이해하는 것은 당신의 process가 실제로 무엇을 최적화하는지에 대해 중요한 무언가를 말해준다. 대부분의 팀은 이 선택을 명시적으로 한 적이 없다. 그들은 수십 년 전 velocity를 correctness보다 선택한 업계로부터 이를 물려받았고, 뒤를 돌아보지 않았다.