3장이 세 패러다임은 저마다 프로그래머에게서 무언가를 빼앗는다고 예고했다. 4장은 그 첫 번째 강탈의 현장으로 우리를 데려간다. 구조적 프로그래밍이 앗아간 것은 goto다. 그런데 이 장의 진짜 이야기는 문법 하나가 금지된 사건이 아니라, 한 야심 찬 수학자가 프로그램을 증명하려다 실패하고, 그 실패의 잔해에서 오늘날 우리가 여전히 기대고 있는 도구 하나를 건져 올린 과정에 있다. 그 도구의 이름은 증명이 아니라 반증이다.

다익스트라의 야심 — 프로그램을 증명하려는 꿈

1968년, 에츠허르 다익스트라는 하나의 원대한 목표를 품고 있었다. 소프트웨어를 수학의 한 분과처럼 다루고 싶었던 것이다. 수학자가 정리를 세우고 공리로부터 한 단계씩 증명을 쌓아 올려 그 정리가 참임을 의심의 여지 없이 확정하듯, 프로그래머도 자신의 코드가 옳다는 것을 증명해 낼 수 있어야 한다고 그는 믿었다. 버그란 결국 증명되지 않은 코드를 세상에 내보낸 대가이며, 만약 프로그램의 정확성을 수학적으로 증명할 수만 있다면 우리는 버그 없는 소프트웨어의 시대로 들어설 수 있으리라는 꿈이었다.

이 꿈이 왜 그토록 매력적인지는 자명하다. 소프트웨어는 규모가 조금만 커져도 인간의 머리로 전체를 한꺼번에 붙들 수 없을 만큼 복잡해진다. 다익스트라는 이 복잡성을 다스리는 유일하게 검증된 방법이 수학의 증명 기법이라고 보았다. 큰 정리를 작은 보조정리들로 쪼개고, 각 보조정리를 엄밀히 증명한 뒤 그것들을 결합해 전체를 세우는 분할 정복. 그는 프로그램에도 이 방식을 그대로 적용하려 했다.

goto가 증명을 가로막는다

증명을 시도하던 다익스트라는 곧 장애물 하나와 마주친다. goto 문이었다. 당시의 프로그램은 제어 흐름을 임의의 지점으로 뛰어넘기는 goto로 가득했고, 이 무절제한 점프가 코드를 증명 불가능하게 만들었다.

이유는 이렇다. 증명은 프로그램을 작은 단위로 쪼개고, 각 단위가 옳음을 보인 뒤 그것들을 합쳐 전체를 세우는 방식으로 진행된다. 그런데 goto가 어떤 코드 블록의 한복판으로 아무렇게나 뛰어들 수 있다면, 그 블록은 더 이상 독립된 단위가 아니다. 바깥의 어느 지점에서 흐름이 들이닥칠지 모르는 조각은 그 자체만 떼어 놓고 “이것은 이런 입력에 이런 출력을 낸다”고 단정할 수 없다. 경계가 없는 코드는 증명의 단위가 되지 못한다.

다익스트라는 goto가 쓰이는 방식을 살펴보다 흥미로운 사실을 발견한다. goto의 용례 중 상당수는 사실 더 정제된 제어 구조 — 조건 분기와 반복 — 로 얼마든지 대체할 수 있는 것들이었다. 문제가 되는 것은 오직 그 두 구조로 환원되지 않는 나머지, 블록의 경계를 함부로 넘나드는 점프들이었다. 바로 그것들이 코드를 잘게 나누어 증명하는 일을 불가능하게 만드는 주범이었다.

판단 기준: 어떤 코드 조각을 “그 자체만 떼어 놓고 옳은지 따질 수 있는가”로 물어보라 — 바깥에서 흐름이 아무 데로나 뛰어들 수 있으면 그 조각은 추론의 단위가 못 된다. 함정: goto가 사라진 오늘날에도 이 병은 형태를 바꿔 살아 있다. 전역 상태를 여기저기서 건드리거나, 깊은 곳에서 흐름을 위로 낚아채는 예외를 남발하면, 문법상 구조적이어도 코드는 다시 “경계 없는 조각”으로 돌아간다.

세 구조로 충분하다 — 뵘과 야코피니

다익스트라의 통찰에는 든든한 이론적 뒷받침이 있었다. 그보다 앞서 코라도 뵘과 주세페 야코피니가, 어떤 프로그램이든 단 세 가지 제어 구조만으로 표현할 수 있음을 증명해 두었던 것이다. 그 세 가지는 **순차(sequence), 분기(selection), 반복(iteration)**이다.

순차는 문장을 위에서 아래로 차례로 실행하는 것, 분기는 조건에 따라 갈래를 고르는 것(if/then/else), 반복은 조건이 참인 동안 같은 블록을 되풀이하는 것(while, for)이다. 놀라운 점은 이 셋이 전부라는 사실이다. 아무리 복잡하게 얽힌 로직이라도, 아무리 정교한 알고리즘이라도, 이 세 구조의 조합으로 빠짐없이 나타낼 수 있다. goto로만 할 수 있는 일이란 없었다. 무절제한 점프는 편의였을 뿐 필수가 아니었다.

이 정리가 다익스트라의 프로그램에 결정적이었던 이유는, 이 세 구조가 모두 입구가 하나, 출구가 하나인 블록이기 때문이다. 순차 블록도, 분기 블록도, 반복 블록도 흐름은 반드시 위에서 들어와 아래로 나간다. 옆구리로 뛰어들거나 중간에서 새어 나가는 문이 없다. 그래서 각 블록은 독립된 단위로 도려낼 수 있고, 도려낼 수 있으니 하나씩 그 정확성을 따질 수 있으며, 따진 것들을 다시 순차로 이어 붙여 더 큰 블록의 정확성을 세울 수 있다. 세 구조는 증명을 가능케 하는 최소한의 벽돌이었다.

판단 기준: 새 제어 흐름을 짤 때 “이 블록은 한 곳으로 들어와 한 곳으로 나가는가”를 물어라 — 단일 입구·단일 출구가 지켜지면 그 블록은 나중에 통째로 함수로 뽑아내거나 따로 테스트할 수 있는 단위가 된다. 함정: 세 구조만으로 짜라는 말을 “함수 중간 return 금지” 같은 교조로 굳히지 말 것. 요점은 문법 규칙이 아니라 추론 가능한 경계다 — 이른 반환이 오히려 블록을 명료하게 가른다면 그것이 세 구조의 정신에 더 가깝다.

goto 유해론 — 규율이 된 통찰

다익스트라는 이 발견을 1968년 「Go To Statement Considered Harmful」이라는 짧은 편지로 발표한다. 프로그래밍 역사에서 가장 유명한 제목 중 하나가 된 이 글은, 당대에는 격렬한 반발을 샀다. goto를 손에서 놓지 못하던 프로그래머들에게 이것은 자신의 자유를 빼앗는 참견으로 들렸다.

그러나 3장의 렌즈로 보면 이 강탈의 성격이 분명해진다. 구조적 프로그래밍은 프로그래머에게서 goto를 앗아갔다. 무엇을 대가로? 프로그램을 증명 가능한(그리고 분해 가능한) 단위로 쪼갤 수 있는 규율을 얻는 대가로. 패러다임이 제약을 부과할 때, 그 제약은 우리를 옥죄는 것이 아니라 특정한 종류의 나쁜 코드를 아예 쓸 수 없게 만들어 우리를 지킨다. goto를 뺏김으로써 우리는 경계 없는 스파게티 코드를 원천적으로 쓸 수 없게 되었고, 그 자리에 하나로 들어와 하나로 나가는 정연한 블록들의 위계를 얻었다.

오늘날 이 논쟁은 사실상 끝났다. 현대의 어떤 주류 언어도 무절제한 goto를 권하지 않으며, 우리는 ifwhile을 너무 당연하게 써서 그것이 한때 치열한 논쟁의 대상이었다는 사실조차 잊고 산다. 승부는 났고, 우리는 모두 구조적 프로그래머다.

증명은 끝내 오지 않았다

여기까지라면 아름다운 성공담이다. 그러나 이 장의 반전이 여기서 시작된다. 다익스트라가 goto를 몰아내고 세 구조를 정련한 것은 어디까지나 더 큰 목표 — 프로그램의 정확성을 수학적으로 증명하는 것 — 를 위한 준비 작업이었다. 그런데 그 최종 목표는 끝내 이루어지지 않았다.

프로그램을 유클리드 기하학의 정리처럼 엄밀히 증명하려는 시도는 무겁고 번거로웠다. 코드 한 조각의 정확성을 형식적으로 증명하는 일은 그 코드를 짜는 일보다 훨씬 큰 노력을 요구했고, 실무의 규모에서는 감당할 수 없었다. 학계의 일부가 이 방향을 밀어붙였지만, 프로그래밍 산업 전체가 코드를 수학적으로 증명하며 개발하는 세계는 오지 않았다. 다익스트라의 원대한 꿈은, 적어도 그가 상상한 형태로는, 실현되지 못한 채 남았다.

판단 기준: 어떤 방법론이 “이론적으로 완벽하지만 실무 규모에서 비용이 감당 안 되는가”를 물어라 — 완벽한 증명보다 값싼 반증이 살아남는 것은 옳아서가 아니라 치를 수 있는 값이어서다. 함정: 증명이 실패했다는 사실을 “엄밀함은 헛되다”로 오독하지 말 것. 실패한 것은 형식 증명이라는 특정 수단이지, 코드를 작은 단위로 쪼개 하나씩 따진다는 정신이 아니다 — 그 정신은 반증이라는 더 값싼 형태로 살아남았다.

과학이라는 대안 — 증명이 아니라 반증

증명이 좌절된 자리에서 마틴은 훨씬 더 실용적인 대안을 끌어온다. 바로 과학적 방법이다. 수학과 과학은 진리를 다루는 방식이 근본적으로 다르다.

수학은 증명의 학문이다. 명제가 참임을 공리로부터 연역해 확정한다. 반면 과학은 결코 무언가가 참임을 증명하지 못한다. 과학이 하는 일은 정반대다. 어떤 이론을 세우고, 그 이론을 깨뜨리려 온갖 실험으로 두들긴다. 아무리 두들겨도 깨지지 않으면 그 이론은 “지금까지는 참인 것 같다”는 잠정적 지위를 얻는다. 뉴턴의 역학이 수백 년간 그 자리를 지키다 상대성 이론 앞에서 반증되었듯, 과학의 진리는 언제든 반증에 열려 있는 잠정적 진리다. 과학 이론은 **반증 가능(falsifiable)**하기에 과학이다. 반증할 방법이 아예 없는 주장은 과학이 아니다.

마틴의 통찰은 소프트웨어가 수학이 아니라 과학을 닮았다는 것이다. 우리는 프로그램이 옳다는 것을 증명할 수 없다. 우리가 할 수 있는 최선은, 프로그램이 틀렸음을 보이려 애쓰고 — 즉 테스트하고 — 그 시도가 실패하는(버그를 못 찾는) 한 그 프로그램을 “충분히 옳다”고 잠정적으로 신뢰하는 것이다.

테스트는 버그의 존재만 보인다

이 관점을 다익스트라 자신의 가장 유명한 문장이 못박는다. “테스트는 버그의 존재를 보여 줄 수 있을 뿐, 버그의 부재를 결코 증명하지 못한다.”

이 한 문장에 4장의 핵심이 압축되어 있다. 테스트를 아무리 많이 통과시켜도, 그것은 “이 테스트들이 건드린 경우에는 버그가 없더라”를 말할 뿐이다. 테스트가 건드리지 않은 무수한 경우들 어딘가에 버그가 숨어 있을 가능성은 조금도 줄어들지 않는다. 테스트를 백 개 통과한 코드는 백 개의 반증 시도를 견뎌 낸 것이지, 옳음이 증명된 것이 아니다. 반대로 테스트가 단 하나라도 실패하면, 그것은 즉시 버그의 존재를 확정한다. 테스트는 이렇게 비대칭이다 — 실패는 확정이고, 성공은 잠정이다.

그래서 잘 짠 테스트란 프로그램의 정확성을 증명하는 도구가 아니라, 프로그램을 반증하려는 공격이다. 좋은 테스트는 코드를 깨뜨리려고 가장 짓궂은 입력으로 두들긴다. 그렇게 두들겨도 깨지지 않을 때, 우리는 비로소 그 코드를 배포할 만큼 신뢰한다. 이것이 소프트웨어가 다룰 수 있는 최선의 “옳음”이며, 다익스트라가 증명의 꿈을 좇다 얻은 뜻밖의 유산이다.

판단 기준: 테스트를 짤 때 “이 테스트가 통과하면 무엇이 증명되나”가 아니라 “이 테스트가 어떤 버그를 잡아낼 수 있나”를 물어라 — 반증하지 못하는 테스트(무엇을 넣어도 통과하는 테스트)는 안심을 줄 뿐 아무것도 지키지 못한다. 함정: 테스트가 다 초록불이라고 “버그가 없다”고 말하지 말 것. 정확히 말하면 “지금까지의 반증 시도가 모두 실패했다”이며, 그 미묘한 차이가 방심과 경계를 가른다.

기능적 분해 — 반증 가능한 단위로 쪼개기

증명이 반증으로 대체되자, 세 구조의 쓸모도 자리를 옮긴다. 다익스트라에게 세 구조는 코드를 증명 가능한 단위로 쪼개는 도구였다. 마틴에게 세 구조는 코드를 반증 가능한, 즉 개별적으로 테스트 가능한 단위로 쪼개는 도구가 된다.

구조적 프로그래밍은 큰 문제를 고수준의 기능으로 나누고, 그 기능을 다시 저수준의 기능으로 나누는 하향식 분해를 자연스럽게 허락한다. 단일 입구·단일 출구를 지키는 함수들의 위계 덕분에, 우리는 거대한 시스템을 잎사귀 하나하나까지 잘게 쪼갤 수 있다. 그리고 그 잎사귀 하나하나는 독립된 반증의 대상이 된다 — 따로 떼어 테스트로 두들겨 볼 수 있는 작은 단위. 함수가 하나로 들어와 하나로 나가는 정연한 블록이기에, 우리는 “이 입력에 이 출력이 나와야 한다”는 명료한 반증 조건을 그 함수에 걸 수 있다.

이 분해가 없다면 반증은 불가능하다. goto로 얽힌 스파게티는 떼어 낼 잎사귀가 없어서, 시스템 전체를 한 덩어리로만 테스트할 수 있을 뿐이다. 구조적 프로그래밍이 준 잘게 쪼개는 능력이 있어야, 과학적 방법을 코드의 구석구석에 적용할 수 있다. 세 구조 → 기능적 분해 → 반증 가능한 단위 → 테스트. 이 사슬이 4장의 논증 전체다.

판단 기준: 함수를 쪼갤 때 “이 조각에 실패하는 테스트를 하나라도 상상할 수 있는가”를 물어라 — 반증 조건을 걸 수 없을 만큼 애매하거나 부수효과에 얽힌 조각이라면, 그 경계가 잘못 그어진 것이다. 함정: 잘게 쪼개는 것 자체를 목적으로 삼지 말 것. 5장에서 볼 자율적 협력처럼, 분해는 각 조각이 독립적으로 검증 가능해질 때 값을 하며, 검증할 수 없는 조각을 늘리는 분해는 대가만 남는다.

아키텍트에게 주는 것 — 반증 가능성

마틴이 이 장을 아키텍처 책에 넣은 이유는 마지막 한 걸음에 있다. 구조적 프로그래밍이 함수 수준에서 준 것 — 반증 가능성 — 이 사실은 아키텍처의 모든 수준에서 아키텍트가 추구해야 할 바로 그것이기 때문이다.

좋은 아키텍트는 시스템을 개별적으로 테스트 가능한, 다시 말해 반증 가능한 단위로 나눈다. 함수를 세 구조로 쪼개듯, 아키텍트는 시스템을 컴포넌트로, 컴포넌트를 다시 모듈로 쪼갠다. 그리고 각 수준에서 던지는 질문은 다익스트라의 질문과 똑같다 — “이 단위를 따로 떼어 그것이 틀렸음을 보이려 시도할 수 있는가?” 떼어 낼 수 없는 단위, 반증할 수 없는 단위는 아키텍처의 경계가 잘못 그어졌다는 신호다.

그래서 구조적 프로그래밍은 단지 함수를 짜는 옛 규율이 아니다. 그것은 아키텍처가 작동하는 원리의 축소판이다. 큰 것을 반증 가능한 작은 것들로 쪼개고, 각각을 과학적으로 두들겨 신뢰를 쌓고, 신뢰할 수 있는 조각들을 이어 붙여 전체의 신뢰를 세운다. 다익스트라의 증명은 실패했지만, 그가 프로그램을 분해 가능한 단위로 만들려 벼려 낸 도구는 아키텍트의 손에서 여전히 가장 근본적인 무기로 쓰인다.

  • 다익스트라는 프로그램을 수학처럼 증명하려 했고, goto가 그 증명을 가로막는 것을 발견해 몰아냈다. 구조적 프로그래밍이 앗아간 것은 goto, 그 대가로 얻은 것은 코드를 분해 가능한 단위로 쪼개는 규율이다.
  • 뵘과 야코피니는 순차·분기·반복 세 구조만으로 모든 프로그램을 표현할 수 있음을 증명했다. 셋 다 단일 입구·단일 출구여서 독립된 추론의 단위가 된다.
  • 증명의 꿈은 실무 규모에서 실현되지 못했다. 그 자리를 과학적 방법 — 증명이 아니라 반증 — 이 대신했다. 소프트웨어는 수학이 아니라 과학을 닮았다.
  • “테스트는 버그의 존재를 보일 뿐 부재를 증명하지 못한다.” 실패는 확정, 성공은 잠정. 테스트는 옳음의 증명이 아니라 반증의 공격이다.
  • 구조적 프로그래밍이 아키텍트에게 주는 것은 반증 가능성이다 — 시스템을 개별적으로 테스트 가능한 단위로 쪼개는 능력, 이것이 아키텍처의 모든 수준을 관통한다.

증명의 꿈이 반증의 규율로 착지하는 이 장은, 패러다임의 제약이 어떻게 아키텍트의 무기가 되는지를 보여 준 첫 사례다. 다음 장은 두 번째 강탈로 넘어간다 — 객체 지향이 함수 포인터를 앗아가고, 그 자리에서 아키텍트가 얻는 훨씬 더 큰 권력, 곧 의존성의 방향을 뒤집는 힘에 관한 이야기다.

다음장으로 5장