존 오스터하우트의 실용적 소프트웨어 설계 관점, Tcl의 유산, 오스터하우트와 브룩스 논쟁, 그리고 복잡성이 제품에 미치는 비용을 살펴봅니다.

존 오스터하우트는 연구와 실제 시스템을 오가는 컴퓨터 과학자이자 엔지니어입니다. 그는 Tcl 프로그래밍 언어를 만들고 현대 파일 시스템 발전에 기여했으며, 수십 년의 경험을 단순하지만 약간 불편한 주장으로 정리했습니다: 복잡성은 소프트웨어의 주된 적이다.
이 메시지는 여전히 유효합니다. 대부분의 팀이 실패하는 이유는 기능 부족이나 노력 부족이 아니라, 시스템(그리고 조직)이 이해하기 어렵고 변경하기 어렵고 쉽게 고장나도록 되는 데 있습니다. 복잡성은 단순히 엔지니어의 속도를 늦추지 않습니다. 제품 결정, 로드맵 신뢰도, 고객 신뢰, 사고 빈도, 심지어 채용에도 스며듭니다 — 온보딩이 몇 달짜리 일이 되기 때문입니다.
오스터하우트의 관점은 실용적입니다: 시스템이 특수 경우, 예외, 숨겨진 의존성, "이번만" 식의 수정을 쌓을 때 비용은 코드베이스에만 국한되지 않습니다. 제품 전체가 진화하기 더 비싸집니다. 기능 개발은 느려지고 QA는 어려워지며 릴리스는 더 위험해집니다. 팀은 어떤 것도 건드리기 두려워 개선을 회피하게 됩니다.
이건 학문적 순수성을 요구하는 주장이 아닙니다. 모든 지름길에는 이자가 붙는다는 사실을 상기시키는 것입니다 — 복잡성은 가장 이자가 높은 부채입니다.
아이디어를 구체적으로 만들기 위해(그리고 단순히 동기 부여로 끝나지 않게), 오스터하우트의 메시지를 세 가지 각도에서 살펴보겠습니다:
이 글은 단지 언어 애호가를 위한 것이 아닙니다. 제품을 만들고, 팀을 이끌고, 로드맵의 트레이드오프를 결정하는 사람이라면 복잡성을 조기에 포착하고 제도화되는 것을 막으며 단순함을 1순위 제약으로 취급하는 실용적 방법을 얻을 수 있습니다.
복잡성은 “코드가 많다”거나 “수학이 어렵다”는 뜻이 아닙니다. 그것은 당신이 변경할 때 시스템이 어떻게 동작할지 생각하는 것과 실제로 동작하는 것 사이의 격차입니다. 작은 수정이 위험하게 느껴진다면—그 이유는 폭발 반경을 예측할 수 없기 때문입니다.
건강한 코드에서는 다음 질문에 답할 수 있어야 합니다: “이걸 바꾸면 무엇이 깨질 수 있나?” 복잡성은 이 질문을 비싸게 만드는 것입니다.
복잡성은 흔히 다음에 숨어 있습니다:
팀은 복잡성을 느린 배포(조사에 더 많은 시간 소요), 더 많은 버그(동작이 놀랍기 때문), 그리고 깨지기 쉬운 시스템(변경을 위해 많은 사람과 서비스의 조정 필요)로 체감합니다. 또한 온보딩에 세금이 붙습니다: 새 팀원이 정신적 모델을 구성하지 못하면 핵심 흐름을 건드리지 않게 됩니다.
일부 복잡성은 본질적입니다: 비즈니스 규칙, 규정 준수 요구, 현실 세계의 엣지 케이스. 그건 삭제할 수 없습니다.
하지만 많은 부분은 우연적입니다: 혼란스러운 API, 중복된 로직, 영구화된 "임시" 플래그, 내부 세부를 누설하는 모듈. 이것이 설계 선택이 만들어내는 복잡성이고, 일관되게 줄일 수 있는 유일한 종류입니다.
Tcl은 실용적 목표로 시작했습니다: 소프트웨어를 쉽게 자동화하고 기존 애플리케이션을 재작성하지 않고 확장하게 하는 것. 오스터하우트는 팀이 "충분한 수준의 프로그래밍 가능성"을 도구에 추가하고, 그 힘을 사용자·운영자·QA 등 스크립팅이 필요한 누구에게든 제공할 수 있도록 설계했습니다.
Tcl은 글루 언어(glue language)의 개념을 대중화했습니다: 더 빠르고 낮은 수준 언어로 작성된 구성 요소를 연결하는 작고 유연한 스크립팅 레이어. 모든 기능을 모노리스에 넣는 대신, 명령 집합을 노출하고 이를 조합해 새로운 동작을 만들 수 있었습니다.
이 모델이 영향력을 얻은 이유는 실제 업무 흐름과 맞았기 때문입니다. 사람들은 단지 제품만 만들지 않습니다; 빌드 시스템, 테스트 하니스, 관리자 도구, 데이터 변환기, 일회성 자동화도 만듭니다. 가벼운 스크립팅 레이어는 이러한 작업을 "티켓을 올리는 일"에서 "스크립트 작성"으로 바꿉니다.
Tcl은 임베딩을 1급 고려사항으로 만들었습니다. 인터프리터를 애플리케이션에 넣고 명령 인터페이스를 노출하면 즉시 구성 가능성과 빠른 반복을 얻을 수 있었습니다.
이 패턴은 플러그인 시스템, 구성 언어, 확장 API, 임베디드 스크립팅 런타임 등 오늘날에도 나타납니다—문법이 Tcl 같지 않더라도 동일한 설계 습관을 반영합니다.
또한 안정적인 원시 기능(호스트 앱의 핵심 기능)을 가변적 구성(스크립트)과 분리하는 습관을 강화했습니다. 이 방식이 잘 작동하면 도구는 핵심을 계속 불안정하게 만들지 않고 빠르게 진화할 수 있습니다.
Tcl의 문법과 "모든 것을 문자열로" 처리하는 모델은 직관적이지 않게 느껴질 수 있었고, 큰 Tcl 코드베이스는 강한 규약 없이는 이해하기 어려워질 수 있습니다. 더 풍부한 표준 라이브러리, 더 나은 툴링, 더 큰 커뮤니티를 제공하는 새로운 에코시스템이 등장하면서 많은 팀이 자연스럽게 이동했습니다.
그럼에도 Tcl의 유산은 지워지지 않습니다: 확장성과 자동화가 단지 부가 기능이 아니라 제품 기능으로서 유지·운영 비용을 크게 줄일 수 있다는 관념을 정착시켰습니다.
Tcl은 교묘하게 엄격한 아이디어로 만들어졌습니다: 핵심은 작게 유지하고, 조합을 강력하게 만들며, 스크립트는 사람들이 지속적으로 번역 없이 함께 작업할 수 있을 만큼 읽기 쉽게 유지하자.
Tcl은 많은 특화된 기능을 제공하기보다 소수의 원시 기능(문자열, 명령, 간단한 평가 규칙)에 의존해 사용자가 조합하도록 기대했습니다.
이 철학은 설계자가 더 적은 개념을 여러 맥락에서 재사용하도록 유도합니다. 제품 및 API 설계에 대한 교훈은 간단합니다: 두세 개의 일관된 빌딩 블록으로 열 가지 요구를 해결할 수 있다면, 사람들이 배워야 할 표면적을 줄이는 것입니다.
소프트웨어 설계의 주요 함정은 빌더의 편의성에 최적화하는 것입니다. 기능은 구현은 쉬울 수 있지만(기존 옵션 복사, 특수 플래그 추가, 구석 패치) 제품을 사용하기 더 어렵게 만들 수 있습니다.
Tcl은 반대에 방점을 찍었습니다: 정신 모델을 단단하게 유지하라, 구현이 뒤에서 더 많은 일을 하더라도. 제안서를 검토할 때 묻자: 이것이 사용자가 기억해야 할 개념 수를 줄이는가, 아니면 하나의 예외를 더 추가하는가?
원시(primitive)가 일관될 때만 최소주의가 도움이 됩니다. 두 명령이 비슷해 보이지만 엣지 케이스에서 다르게 동작하면 사용자는 퀴즈를 외워야 합니다. 적은 도구 세트는 규칙이 미묘하게 다르면 "날카로운 모서리"가 될 수 있습니다.
주방을 생각해보세요: 좋은 칼, 팬, 오븐이면 여러 요리를 만들 수 있습니다. 아보카도 전용 슬라이서 같은 도구는 일회성 기능입니다—팔기 쉽지만 서랍을 어지럽힙니다.
Tcl의 철학은 칼과 팬을 권합니다: 일반 도구들이 깔끔하게 조합되어야 새로운 레시피마다 도구가 필요하지 않습니다.
1986년 프레드 브룩스는 도발적인 결론의 에세이를 썼습니다: 소프트웨어 개발을 한 번에 획기적으로 더 빠르고 싸고 신뢰성 있게 만들 마법 같은 단일 돌파구는 없다.
그의 요지는 진보가 불가능하다는 것이 아닙니다. 소프트웨어는 이미 거의 모든 것을 할 수 있는 매체고, 그 자유는 독특한 부담을 동반한다: 우리는 만들면서 끊임없이 무엇을 만들지 정의하고 있다는 것입니다. 더 나은 도구는 도움이 되지만 가장 어려운 부분을 제거하지는 않습니다.
브룩스는 복잡성을 두 가지로 나눴습니다:
도구는 우연적 복잡성을 없앨 수 있습니다. 고급 언어, 버전 관리, CI, 컨테이너, 관리형 데이터베이스, 좋은 IDE에서 얻은 이득을 생각해보세요. 그러나 브룩스는 본질적 복잡성이 지배적이라고 주장했고, 도구가 좋아져도 그것이 사라지지는 않는다고 말했습니다.
현대 플랫폼에서도 팀은 대부분의 에너지를 요구사항 협상, 시스템 통합, 예외 처리, 시간에 걸친 일관성 유지에 씁니다. 표면은 바뀔 수 있지만(디바이스 드라이버 대신 클라우드 API), 핵심 과제는 그대로입니다: 인간의 요구를 정확하고 유지보수 가능한 동작으로 번역하는 일입니다.
이것이 오스터하우트가 파고드는 긴장입니다: 본질적 복잡성을 삭제할 수 없다면, 규율 있는 설계가 얼마나 의미 있게 "얼마나 많이 그 복잡성이 코드로—그리고 개발자의 머릿속으로—새는지"를 줄일 수 있겠는가?
사람들은 때때로 “오스터하우트 vs 브룩스”를 낙관주의와 현실주의의 싸움으로 프레이밍합니다. 더 유용한 관점은 두 경험 많은 엔지니어가 동일한 문제의 다른 부분을 설명하고 있다는 것입니다.
브룩스의 “No Silver Bullet”은 마법 같은 한 방이 없다고 주장합니다. 오스터하우트는 그것을 부정하지 않습니다.
그의 반박은 더 좁고 실용적입니다: 팀은 종종 복잡성을 불가피한 것으로 대합니다. 많은 복잡성은 자초한 것입니다.
오스터하우트 관점에서 좋은 설계는 복잡성을 의미 있게 줄일 수 있습니다—소프트웨어를 "쉽게" 만드는 것이 아니라 변경하기 덜 혼란스럽게 만드는 것입니다. 혼란이 일상 작업을 느리게 만드는 주범이기 때문에 이는 큰 주장입니다.
브룩스는 본질적 어려움에 집중합니다: 소프트웨어는 복잡한 현실, 변화하는 요구, 코드 밖에 존재하는 엣지 케이스를 모델링해야 합니다. 좋은 도구와 똑똑한 사람으로도 그것을 삭제할 수는 없습니다. 관리할 뿐입니다.
논쟁보다 더 많은 부분에서 겹칩니다:
“누가 옳은가?”라고 묻는 대신, 질문하세요: 이번 분기에 우리가 통제할 수 있는 복잡성은 무엇인가?
팀은 시장 변화나 도메인의 핵심 난이도를 통제할 수 없습니다. 하지만 새로운 기능이 특수 사례를 더 추가하는지, API가 호출자에게 숨겨진 규칙을 강요하는지, 모듈이 복잡성을 숨기거나 누설하는지는 통제할 수 있습니다.
이것이 실행 가능한 중간 지점입니다: 본질적 복잡성을 받아들이고 우연적 복잡성에 대해 무자비하게 선택하라.
딥 모듈은 많은 일을 하면서 작고 이해하기 쉬운 인터페이스를 노출하는 구성 요소입니다. “깊이”는 모듈이 당신의 접시에서 떠맡는 복잡성의 양입니다: 호출자는 지저분한 세부를 알 필요가 없고 인터페이스도 이를 강요하지 않습니다.
얕은 모듈은 그 반대입니다: 작은 로직을 감싸지만 많은 매개변수, 특수 플래그, 호출 순서 요구, 또는 “기억해야 할 것” 규칙을 통해 복잡성을 밖으로 밀어냅니다.
레스토랑을 생각해보세요. 딥 모듈은 주방입니다: 메뉴에서 "파스타"를 주문하면 공급업체 선택, 삶는 시간, 플레이팅을 신경 쓰지 않습니다.
얕은 모듈은 재료와 12단계 조리법을 주고 팬도 직접 가져오라 하는 "주방"입니다. 일은 여전히 일어나지만 고객에게 옮겨졌습니다.
추가 레이어는 여러 결정을 하나의 명확한 선택으로 압축할 때 훌륭합니다.
예컨대 재시도, 직렬화, 인덱싱을 내부에서 처리하는 save(order)를 제공하는 저장소 레이어는 딥합니다.
반면 레이어가 주로 이름만 바꾸거나 옵션을 추가한다면 해롭습니다. 새 추상이 제거하는 것보다 더 많은 구성을 도입하면—예: save(order, format, retries, timeout, mode, legacyMode)—얕을 가능성이 큽니다. 코드가 "정리된" 것처럼 보여도 인지 부하는 모든 호출 지점에서 드러납니다.
useCache, skipValidation, force, legacy 같은 불리언이 있다.딥 모듈은 단순히 코드를 캡슐화하지 않습니다. 결정을 캡슐화합니다.
"좋은" API는 단지 많은 일을 할 수 있는 것이 아닙니다. 그것은 사람들이 작업 중 머릿속에 담아둘 수 있는 API입니다.
오스터하우트의 설계 렌즈는 API를 기억해야 할 규칙 수, 예외 수, 실수로 잘못 사용할 가능성으로 평가하라고 압박합니다.
사람 친화적 API는 보통 작고, 일관되며, 오용하기 어렵다.
작다는 것은 무능함을 의미하지 않습니다—표면은 적지만 몇 가지 개념으로 잘 조합됩니다. 일관성은 전체 시스템에 같은 패턴이 통하는 것(매개변수, 오류 처리, 명명, 반환 타입 등)을 말합니다. 오용하기 어렵다는 것은 안전한 경로로 유도하는 API: 경계에서의 검증, 명확한 불변식, 유형 또는 런타임 체크로 조기 실패를 유도합니다.
추가되는 플래그, 모드, "혹시 몰라" 구성은 모든 사용자에게 세금이 됩니다. 호출자의 5%만 필요해도, 100%는 이제 그 옵션의 존재를 배우고, 자신이 필요로 하는지 고민하고, 다른 옵션과 상호작용할 때 동작을 해석해야 합니다.
이것이 API가 숨겨진 복잡성을 축적하는 방식입니다: 개별 호출이 아니라 조합 수의 폭발에서입니다.
기본값은 친절합니다: 대부분의 호출자가 결정을 생략해도 합리적 동작을 얻을 수 있습니다. 관례(하나의 명확한 방식)는 사용자의 선택 분기를 줄입니다. 명명은 실제로 일을 합니다: 의도에 맞는 동사와 명사를 선택하고 비슷한 작업은 비슷하게 이름 지으세요.
하나 더: 내부 API는 공개 API만큼 중요합니다. 대부분의 제품 복잡성은 백엔드에 있습니다—서비스 경계, 공유 라이브러리, "헬퍼" 모듈. 이 인터페이스들을 제품처럼 다루고 리뷰와 버전 관리 규율을 적용하세요(참고 /blog/deep-modules).
복잡성은 보통 한 번의 "나쁜 결정"으로 오지 않습니다. 마감 압박 속에서 합리적으로 보이는 작은 패치들을 통해 축적됩니다.
하나는 기능 플래그가 도처에 있는 것입니다. 플래그는 안전한 롤아웃에 유용하지만 오래 남으면 각 플래그가 가능한 동작 수를 곱합니다. 엔지니어들은 "시스템"이 아니라 "플래그 A가 켜지고 사용자 세그먼트 B에 있을 때의 시스템"을 생각하기 시작합니다.
또 다른 함정은 특수 사례 로직입니다: "엔터프라이즈 고객은 X가 필요하다", "지역 Y에서는 예외", "계정이 90일 전이면 예외" 등. 이런 예외들은 코드베이스 전반에 퍼지고 몇 달 후에는 어떤 것이 여전히 필요한지 아무도 모릅니다.
세 번째는 누설하는 추상화입니다. API가 호출자에게 내부 세부(타이밍, 저장 형식, 캐싱 규칙)를 이해하도록 강요하면 복잡성은 밖으로 밀려납니다. 한 모듈이 부담을 지는 대신 모든 호출자가 그 특이성을 배우게 됩니다.
전술적 프로그래밍은 이번 주에 최적화합니다: 빠른 수정, 최소 변경, "그냥 패치".
전략적 프로그래밍은 내년을 최적화합니다: 반복적으로 발생하는 버그를 방지하고 미래 작업을 줄이는 작은 재설계를 합니다.
위험은 "유지보수 이자"입니다. 빠른 임시방편은 지금은 싸게 느껴지지만 이자는 나중에 지불합니다: 온보딩 느려짐, 취약한 릴리스, 아무도 오래된 코드를 건드리고 싶어하지 않는 공포 기반 개발.
코드 리뷰에 가벼운 프롬프트를 추가하세요: "이게 새로운 특수 사례를 추가하나?", "API가 이 세부를 숨길 수 있나?", "우리가 남기는 복잡성은 무엇인가?"
비사소한 트레이드오프에 대해 짧은 결정 기록(몇 줄이면 충분)을 남기고, 매 스프린트마다 소규모 리팩터 예산을 예약해 전략적 수리가 본업 외 활동처럼 취급되지 않게 하세요.
복잡성은 엔지니어링에 갇혀 있지 않습니다. 일정, 신뢰성, 고객 경험으로 새어나갑니다.
시스템을 이해하기 어렵게 만들면 모든 변경이 오래 걸립니다. 출시 일정이 늦어지고 각 릴리스마다 더 많은 조정, 더 많은 회귀 테스트, 더 많은 "안전상" 검토 사이클이 필요합니다.
신뢰성도 피해를 봅니다. 복잡한 시스템은 아무도 완전히 예측할 수 없는 상호작용을 만들어 버그를 엣지 케이스로 드러나게 합니다: 쿠폰, 저장된 장바구니, 지역 세금 규칙이 특정 조합일 때만 결제가 실패하는 식입니다. 이런 문제는 재현하기 어렵고 고치기 느립니다.
온보딩은 숨겨진 부담이 됩니다. 새 팀원은 유용한 정신 모델을 만들 수 없으니 위험한 영역을 피하고 이해하지 못한 패턴을 복사하며 의도치 않게 복잡성을 더합니다.
고객은 동작이 코드의 "특수 사례" 때문인지 신경 쓰지 않습니다. 그들은 일관성 없는 경험을 합니다: 설정이 곳곳에서 다르게 적용되고, 흐름이 도달 경로에 따라 바뀌고, 기능이 "대부분의 경우에만" 작동합니다.
신뢰가 떨어지고 이탈이 늘며 채택이 정체됩니다.
지원 팀은 긴 티켓과 상황을 더 많이 묻는 대화로 복잡성 비용을 지불합니다. 운영은 더 많은 알람, 더 많은 런북, 더 신중한 배포로 대가를 치릅니다. 모든 예외는 모니터링하고 문서화하고 설명해야 할 대상이 됩니다.
"한 가지 알림 규칙을 더해달라"는 요청은 빠르게 보일 수 있지만, 동작 분기 추가, UI 문구 추가, 테스트 케이스 증가, 사용자가 잘못 구성할 수 있는 경로 추가를 동반합니다.
반면 기존 알림 흐름을 단순화하면: 규칙 유형을 줄이고, 기본값을 명확히 하며, 웹과 모바일에서 일관된 동작을 제공하면 놀라움이 줄어듭니다—제품 사용이 쉬워지고 지원이 용이하며 진화 속도가 빨라집니다.
복잡성을 성능이나 보안처럼 계획하고 측정하고 보호하세요. 복잡성이 배달 지연으로 드러날 때까지 기다리면 이미 이자를 지불하고 있는 것입니다.
기능 범위와 함께 릴리스가 도입할 수 있는 신규 복잡성의 양을 정의하세요. 예산은 간단할 수 있습니다: "새 개념 순증 없음(새것을 추가하면 하나는 제거)" 또는 "새 통합은 기존 경로를 대체해야 한다" 등.
계획 시 트레이드오프를 명시하세요: 어떤 기능이 세 가지 새 구성 모드와 두 가지 예외를 필요로 한다면, 그것은 기존 개념에 맞는 기능보다 더 많은 비용을 청구해야 합니다.
완벽한 숫자가 필요 없습니다—올바른 방향으로 추세를 보이는 신호면 충분합니다:
릴리스별로 추적하고 결정과 연결하세요: "우리는 두 개의 새 공개 옵션을 추가했다; 그에 상응해 무엇을 제거하거나 단순화했나?"
프로토타입을 "구현 가능한가?"로만 평가하지 마세요. 대신 물어보세요: "이게 사용하기 간단하고 오용하기 어려운가?"
기능에 익숙하지 않은 사람에게 현실적인 과제를 시켜보세요. 성공까지 걸린 시간, 묻는 질문 수, 잘못된 가정이 발생한 지점을 측정하세요. 그 지점들이 복잡성 핫스팟입니다.
현대 빌드 워크플로우는 우연적 복잡성을 줄일 수 있습니다—단, 반복을 빠르게 하고 실수를 되돌릴 수 있어야 합니다. 예를 들어 팀이 내부 도구나 새 흐름을 채팅을 통해 스케치하기 위해 Koder.ai 같은 비브-코딩 플랫폼을 사용할 때, planning mode(의도를 명확히 하기)와 snapshots/rollback(위험한 변경을 빠르게 되돌리기) 같은 기능은 초기 실험이 위험 부담 없이 느껴지게 해줍니다. 프로토타입이 통과하면 소스 코드를 내보내 동일한 "딥 모듈"과 API 규율을 적용할 수 있습니다.
"복잡성 정리" 작업을 정기적(분기별 또는 주요 릴리스당 한 번)으로 만들고 "완료"의 정의를 정하세요:
목표는 추상적 코드 정리가 아니라: 개념 수 감소, 예외 수 감소, 안전한 변경입니다.
오스터하우트의 "복잡성은 적"이라는 생각을 주간 습관으로 바꾸는 몇 가지 동작입니다.
하나의 혼란 유발 서브시스템(온보딩 고통, 반복 버그, "이건 어떻게 작동하나?" 질문이 많은 영역)을 고르세요.
실내 후속 조치로는 기획 시 "복잡성 리뷰"(/blog/complexity-review)와 도구가 우연적 복잡성을 줄이는지 판단하는 간단 점검(/pricing)이 있습니다.
마지막 질문: 이번 주에 단 하나의 특수 사례만 삭제할 수 있다면, 당신은 무엇을 먼저 제거하겠습니까?
Complexity is the gap between what you expect will happen when you change the system and what actually happens.
You feel it when small edits seem risky because you can’t predict the blast radius (tests, services, configs, customers, or edge cases you might break).
Look for signals that reasoning is expensive:
Essential complexity comes from the domain (regulations, real-world edge cases, core business rules). You can’t remove it—only model it well.
Accidental complexity is self-inflicted (leaky abstractions, duplicated logic, too many modes/flags, unclear APIs). This is the part teams can reliably reduce through design and simplification work.
A deep module does a lot while exposing a small, stable interface. It “absorbs” messy details (retries, formats, ordering, invariants) so callers don’t have to.
A practical test: if most callers can use the module correctly without knowing internal rules, it’s deep; if callers must memorize rules and sequences, it’s shallow.
Common symptoms:
legacy, skipValidation, force, mode).Prefer APIs that are:
When you’re tempted to add “just one more option,” first ask whether you can redesign the interface so most callers don’t need to think about that choice at all.
Use feature flags for controlled rollout, then treat them as debt with an end date:
Long-lived flags multiply the number of “systems” engineers must reason about.
Make complexity explicit in planning, not just in code review:
The goal is to force tradeoffs into the open before complexity becomes institutionalized.
Tactical programming optimizes for this week: quick patches, minimal change, “ship it.”
Strategic programming optimizes for the next year: small redesigns that remove recurring classes of bugs and reduce future work.
A useful heuristic: if a fix requires caller knowledge (“remember to call X first” or “set this flag in prod only”), you probably need a more strategic change to hide that complexity inside the module.
Tcl’s lasting lesson is the power of a small set of primitives plus strong composition—often as an embedded “glue” layer.
Modern equivalents include:
The design goal is the same: keep the core simple and stable, and let change happen through clean interfaces.
Shallow modules often look organized but move complexity outward to every caller.