96 min read

A Philosophy of Software Design, 2nd Edition — 한국어 정리본

원서: John K. Ousterhout, A Philosophy of Software Design, 2nd Edition
형식: 한국어 해설형 번역/정리본
용어 원칙: software design 관련 전문 용어는 가능한 한 English 그대로 유지
분량: Markdown 렌더링 방식에 따라 달라질 수 있지만, page break 기준으로 약 60 page 내외로 읽히도록 구성

이 문서는 원문을 문장 단위로 그대로 옮긴 literal translation이 아니라, 책의 핵심 아이디어, argument, example, design principle, red flag를 한국어로 재구성한 study notes입니다. 원문의 구조를 따라가되, 실제 software engineering 실무와 code review에서 바로 쓸 수 있도록 설명과 체크리스트를 덧붙였습니다.

0. 전체 관점: 이 책이 말하려는 것

이 책의 중심 주제는 단순히 “좋은 code를 작성하자”가 아니다. 더 정확히는 software system의 complexity를 어떻게 줄이고, 어쩔 수 없는 complexity를 어디에 둘 것인가에 대한 철학이다. 저자는 software design을 “한 번의 설계 phase에서 끝나는 일”로 보지 않는다. Software는 계속 바뀌고, 요구사항은 개발 도중에 더 분명해지고, 기존 구조의 약점은 실제 implementation을 통해서야 드러난다. 따라서 design은 project의 초기에만 존재하는 문서가 아니라, code를 작성하고 수정하는 모든 순간에 반복되는 활동이다.

책 전체를 관통하는 기준은 실용적이다. 어떤 system이 복잡한지는 기능의 규모나 algorithm의 난이도만으로 판단하지 않는다. 어떤 developer가 어떤 목표를 달성하려고 할 때, 그 system을 이해하고 수정하기가 어렵다면 그 system은 complex하다. 반대로 기능이 많고 내부가 정교하더라도, 사용자가 다루는 interface가 단순하고 변경 지점이 명확하다면 그 system은 상대적으로 simple하다.

이 관점은 “writer의 편의”보다 “reader의 편의”를 우선한다. Code를 처음 작성하는 사람은 자신이 방금 만든 구조를 알고 있기 때문에 그 code가 단순해 보일 수 있다. 그러나 다른 사람이 빠르게 읽고 올바르게 추측할 수 없다면 그 code는 실제로 단순하지 않다. 좋은 software design은 미래의 독자, 유지보수자, API caller, reviewer가 덜 생각해도 되게 만드는 구조를 만드는 일이다.

책에서 반복되는 두 가지 strategy는 다음과 같다. 첫째, complexity 자체를 제거한다. Special case를 줄이고, naming을 precise하게 만들고, consistency를 유지하며, unnecessary exception을 없애는 방식이다. 둘째, complexity를 encapsulate한다. 모든 것을 없앨 수는 없으므로, 어쩔 수 없는 복잡함은 module 내부로 끌어내리고, 외부에는 simple interface만 노출한다. 즉, 좋은 design은 complexity를 system 전체에 흩뿌리지 않고, 적절한 위치에 격리한다.

1. Preface와 책의 사용법

저자는 software design이 경험 많은 engineer의 감각에만 의존해야 하는 분야라고 보지 않는다. 물론 좋은 design에는 경험이 중요하지만, 반복적으로 관찰되는 pattern과 principle이 있다. 이 책은 그런 principle을 명시적으로 언어화하려는 시도다. 특히 Stanford의 software design course에서 학생들의 project를 보며 반복적으로 나타난 문제, design review 중 발견된 mistake, real system의 성공/실패 사례를 기반으로 한다.

이 책을 읽는 좋은 방식은 각 chapter를 독립된 rule 모음으로 외우는 것이 아니라, complexity를 줄이는 하나의 사고방식으로 연결하는 것이다. 예를 들어 Modules Should Be Deep, Information Hiding, General-Purpose Modules are Deeper, Pull Complexity Downwards는 서로 다른 조언처럼 보이지만 모두 같은 방향을 가리킨다. Caller가 알아야 하는 것을 줄이고, module 내부가 조금 더 수고해서 외부의 cognitive load를 낮추라는 뜻이다.

또 하나 중요한 장치는 red flag다. Red flag는 design이 나쁘다는 증거라기보다, 잠시 멈춰서 생각해봐야 한다는 신호다. Shallow Module, Information Leakage, Temporal Decomposition, Pass-Through Method, Vague Name, Nonobvious Code 같은 증상은 codebase에서 자주 나타난다. 이 신호를 발견했을 때 바로 고치지 않더라도, “왜 이 구조가 생겼는지”, “다른 abstraction이 가능하지 않은지”, “이 dependency가 정말 필요한지”를 묻는 습관이 design 실력을 만든다.

책의 조언은 특정 language나 framework에 묶여 있지 않다. Class, method, interface라는 용어가 자주 나오지만, 같은 원리는 library, subsystem, service, kernel module, protocol handler, RPC API, database schema, configuration system에도 적용된다. 중요한 것은 어떤 code unit이든 외부에 보이는 promise와 내부 implementation을 구분할 수 있어야 한다는 점이다.

2. Chapter 1 — Introduction: It’s All About Complexity

저자는 programming을 인류 역사상 가장 순수한 creative activity 중 하나로 본다. Software는 물리 세계의 제약을 거의 받지 않는다. 우리가 상상할 수 있는 behavior를 virtual world 안에 만들 수 있다. 그러나 바로 그 자유 때문에 가장 큰 제약은 programmer의 이해 능력이 된다. System이 커지고 feature가 늘어나면 component 사이의 dependency가 미묘하게 쌓인다. 시간이 지나면 한 developer가 모든 관련 요인을 머릿속에 유지하기 어려워지고, 작은 변경도 많은 부분을 건드리게 된다.

이 책에서 software design의 목표는 complexity를 관리하는 것이다. Good tool, compiler, debugger, IDE, static analyzer, test framework는 도움이 되지만 충분하지 않다. Tool은 이미 존재하는 complexity를 탐색하거나 발견하는 데 도움을 줄 수 있지만, 복잡한 structure 자체를 단순하게 바꾸지는 못한다. 따라서 design의 핵심은 code 구조를 더 simple하고 obvious하게 만드는 데 있다.

저자는 complexity에 대항하는 두 접근을 제시한다. 첫째는 complexity를 제거하는 것이다. Special case를 없애고, identifier를 consistent하게 사용하고, 불필요한 state를 없애고, error case를 줄이는 식이다. 둘째는 complexity를 encapsulate하는 것이다. 모든 세부사항을 모든 사람이 알아야 하면 system은 빠르게 무너진다. Module boundary를 만들고, 내부의 복잡한 implementation을 simple interface 뒤에 숨기면 developer는 전체 system 중 작은 부분만 이해하고도 작업할 수 있다.

여기서 modular design이 나온다. System을 class, method, package, service 같은 module로 나누고, 각 module이 가능한 독립적으로 동작하게 만든다. 이상적인 module은 외부에 적은 정보만 요구하고, 내부에서 많은 일을 처리한다. 즉, 외부 사람에게는 easy-to-use interface를 제공하고, 어려운 세부사항은 implementation 안에 남긴다.

3. Chapter 1의 design lifecycle 관점

Software design은 physical system design과 다르다. 건물이나 다리의 design은 대체로 implementation 전에 큰 구조가 결정되고, 이후에는 변경 비용이 매우 크다. 초기 software engineering에서도 waterfall model은 이와 비슷하게 requirements, design, coding, testing, maintenance를 분리하려 했다. 하지만 software는 requirements 자체가 구현 중에 변하고, initial design의 문제도 implementation 과정에서야 드러나는 경우가 많다.

이 때문에 저자는 incremental development를 긍정적으로 본다. 작게 만들고, 평가하고, 문제를 발견하고, design을 고치는 과정은 software에 잘 맞는다. 그러나 incremental이라는 말이 “feature만 계속 붙여도 된다”는 뜻은 아니다. Incremental development가 성공하려면 각 iteration에서 design 문제를 고쳐야 한다. 단순히 다음 feature를 빠르게 붙이는 데만 집중하면 tactical programming으로 빠지고, system의 complexity는 계속 증가한다.

책의 중요한 message는 design이 특정 phase가 아니라 continuous process라는 점이다. 좋은 developer는 feature를 구현할 때마다 “이 변경이 system을 더 복잡하게 만드는가?”, “이 abstraction은 나중에 다른 feature에도 도움이 되는가?”, “이 interface는 caller에게 불필요한 정보를 요구하는가?”를 묻는다.

이 chapter에서 이미 책 전체의 방향이 정해진다. Software는 계속 복잡해질 수밖에 없다. 하지만 모든 complexity가 같은 것은 아니다. 어떤 complexity는 feature 자체에서 오는 essential complexity이고, 어떤 것은 나쁜 structure에서 오는 accidental complexity다. 이 책은 후자를 줄이는 방법을 다룬다. 그리고 그 방법은 거창한 architecture diagram보다 작은 code decision에서 시작된다.

4. Chapter 2 — The Nature of Complexity: Complexity의 정의

저자는 complexity를 매우 실용적으로 정의한다. Complexity는 software system의 structure와 관련된 무엇이든, system을 이해하고 수정하기 어렵게 만드는 것이다. 어떤 code가 어떻게 동작하는지 이해하기 어렵거나, 작은 improvement를 위해 많은 노력이 필요하거나, bug를 고치면서 다른 bug를 만들기 쉽다면 그 system은 complex하다.

이 정의에서 중요한 점은 complexity가 system의 크기와 동일하지 않다는 것이다. 큰 system이라고 반드시 나쁜 design은 아니다. 큰 system도 module boundary가 명확하고 interface가 단순하면 developer가 작업하는 순간에 느끼는 complexity는 낮을 수 있다. 반대로 작은 system도 dependency가 얽혀 있고 naming이 모호하며 special case가 많으면 매우 complex할 수 있다.

저자는 complexity를 cost-benefit 관점으로도 설명한다. Simple system에서는 큰 improvement도 비교적 적은 effort로 가능하다. Complex system에서는 작은 change도 많은 비용을 요구한다. 따라서 complexity는 생산성을 직접 갉아먹는다. 이 비용은 단지 coding time만이 아니라, debugging, testing, review, onboarding, feature planning의 비용까지 포함한다.

또 하나의 중요한 관점은 complexity가 작업 빈도에 의해 가중된다는 것이다. System 어딘가에 매우 복잡한 algorithm이 있더라도, 그 부분이 stable하고 거의 수정되지 않으며 interface가 단순하다면 전체 system complexity에 미치는 영향은 작을 수 있다. 반대로 매일 수정해야 하는 흔한 path에 작은 혼란이 있으면 그 비용은 매우 크다. 즉, complexity는 “얼마나 복잡한가”뿐 아니라 “얼마나 자주 마주치는가”로 평가해야 한다.

마지막으로 complexity는 writer보다 reader에게 더 잘 보인다. 자신이 방금 작성한 code는 당연해 보인다. 하지만 다른 사람이 그 code를 읽고 잘못 추측한다면, 그 code는 nonobvious하다. 좋은 design은 자기 자신이 이해하는 code가 아니라, 다른 사람이 빠르게 이해할 수 있는 code를 만드는 일이다.

5. Chapter 2 — Complexity의 세 가지 증상

저자는 complexity가 나타나는 대표적 증상을 세 가지로 정리한다.

첫째는 change amplification이다. Conceptually simple한 변경이 code 여러 곳의 수정으로 증폭되는 현상이다. 예를 들어 UI banner color를 바꾸려는데 여러 page의 HTML을 모두 수정해야 한다면, design이 중복과 dependency를 잘못 배치한 것이다. 좋은 design은 한 design decision이 한 곳에 존재하게 만들고, 변경이 해당 위치에서 끝나게 한다.

둘째는 cognitive load다. Developer가 task를 수행하기 위해 알아야 하는 정보량이 지나치게 많으면 system은 complex하다. 어떤 method 하나를 호출하기 위해 hidden precondition, call order, configuration, side effect, concurrency rule을 모두 기억해야 한다면, 실제 code line 수가 적어도 cognitive load는 높다. Cognitive load는 documentation 부족만의 문제가 아니라, interface 자체가 너무 많은 것을 요구하기 때문에 생긴다.

셋째는 unknown unknowns다. 가장 위험한 complexity다. Developer가 무엇을 모르는지조차 모르는 상태에서는, 어떤 부분을 수정해야 하는지, 어떤 invariant를 지켜야 하는지, 어떤 side effect가 있는지 알 수 없다. 그래서 code를 고친 뒤 예상치 못한 곳에서 bug가 발생한다. 좋은 design은 중요한 dependency와 constraint를 잘 드러내며, 모르는 것이 무엇인지 추적할 수 있게 만든다.

이 세 증상 중 가장 무서운 것은 unknown unknowns다. Change amplification은 귀찮지만 눈에 보이고, cognitive load는 힘들지만 인식할 수 있다. Unknown unknowns는 개발자가 위험을 인식하지 못하게 만든다. Code review와 test가 중요한 이유도 여기에 있다. 하지만 test만으로 unknown unknowns를 완전히 해결할 수는 없다. 근본적으로는 design이 dependency를 명확하게 만들고 information hiding을 제대로 해야 한다.

6. Chapter 2 — Complexity의 원인: Dependency와 Obscurity

Complexity의 주요 원인은 dependencyobscurity다. Dependency는 한 부분을 이해하거나 수정하려면 다른 부분에 대한 지식이 필요한 관계다. 모든 dependency가 나쁜 것은 아니다. Module이 협력하려면 dependency는 필연적이다. 문제는 dependency가 많거나, 불필요하거나, 숨겨져 있거나, 여러 방향으로 얽혀 있을 때다.

Dependency를 줄이는 핵심 방법은 design decision을 localize하는 것이다. 어떤 policy, data representation, ordering constraint, error behavior가 여러 module에 흩어져 있으면 변경 비용이 증가한다. 반대로 그 decision을 한 module 내부에 숨기고, 외부에는 stable interface를 제공하면 dependency가 줄어든다.

obscurity는 중요한 정보가 명확하지 않은 상태다. Code가 무슨 일을 하는지, 왜 그렇게 하는지, 어떤 assumption이 있는지 알기 어렵다면 obscurity가 생긴다. Obscurity는 naming, documentation, structure, control flow, API design 모두에서 발생할 수 있다. 특히 code를 읽는 사람이 잘못된 추측을 하게 만드는 ambiguity가 위험하다.

Dependency와 obscurity는 서로 강화된다. Dependency가 있더라도 명확히 드러나면 관리할 수 있다. 그러나 hidden dependency가 생기면 developer는 자신이 지켜야 할 constraint를 알 수 없다. 이것이 unknown unknowns로 이어진다. 반대로 code가 obscure하면 실제 dependency를 발견하기 어렵고, 결국 변경이 더 위험해진다.

Complexity는 보통 큰 실수 하나로 생기는 것이 아니라, 작은 compromise가 누적되어 생긴다. “이번만 빨리 처리하자”, “나중에 정리하자”, “이 special case는 여기서만 쓰인다” 같은 작은 결정이 system 전체에 쌓이면 나중에는 근본적인 refactoring이 어려워진다. 그래서 저자는 작은 design decision에도 주의를 기울이라고 말한다. Complexity는 incremental하게 증가하므로, simplicity도 incremental하게 지켜야 한다.

7. Chapter 3 — Working Code Isn’t Enough

이 chapter의 핵심은 작동하는 code가 충분하지 않다는 것이다. 많은 developer가 feature가 동작하면 task가 끝났다고 생각한다. 하지만 software는 한 번 작성되고 버려지는 것이 아니라 계속 읽히고 수정된다. 오늘 빨리 작성한 code가 내일의 변경을 어렵게 만들면, 전체 project cost는 오히려 증가한다.

저자는 tactical programmingstrategic programming을 대비시킨다. Tactical programming은 당장 보이는 문제를 가장 빠르게 해결하는 데 집중한다. 새 feature를 붙이고, bug를 막고, deadline을 맞추는 데는 효과적일 수 있다. 그러나 구조적 개선을 미루고, interface를 정리하지 않고, special case를 누적하면 complexity가 계속 증가한다. 결국 더 이상 빠르게 개발할 수 없는 상태가 된다.

Strategic programming은 현재 task를 끝내면서도 system design을 조금씩 개선한다. 여기서 중요한 점은 완벽한 architecture를 위해 모든 것을 멈추자는 뜻이 아니라는 것이다. 저자는 작은 investment를 지속적으로 하라고 말한다. 예를 들어 method 하나의 interface를 단순하게 만들거나, naming을 고치거나, duplicate logic을 합치거나, comment를 추가하거나, hidden dependency를 제거하는 작은 일이 strategic investment다.

이 approach는 “clean code를 위해 feature delivery를 희생하자”가 아니다. 오히려 장기적으로 feature delivery를 가능하게 만들기 위한 investment다. Tactical shortcut은 처음에는 빨라 보이지만 system이 커질수록 속도를 떨어뜨린다. Strategic design은 처음에는 조금 느려 보이지만, complexity curve를 낮춰서 장기 생산성을 유지한다.

8. Chapter 3 — 얼마만큼 design에 투자해야 하는가

저자는 모든 task에서 과도한 design 시간을 쓰라고 하지 않는다. 핵심은 지속적인 small investment다. 경험적으로 개발 시간의 일부를 design cleanup과 refactoring에 투자하는 것이 장기적으로 훨씬 싸다. Code를 작성하는 순간 바로 구조를 조금 개선하면 비용이 작지만, 몇 달 뒤 많은 module이 그 구조에 의존하게 된 뒤 고치려면 비용이 커진다.

Startup이나 빠른 product iteration 환경에서는 이 조언이 특히 논쟁적이다. “지금 살아남는 것이 중요하지, design은 나중 문제 아닌가?”라는 반응이 자연스럽다. 저자는 초기 startup도 design을 완전히 무시해서는 안 된다고 본다. 물론 어떤 prototype은 버려질 수 있고, 모든 것을 완벽하게 만들 수는 없다. 그러나 product가 성공하면 codebase는 계속 확장된다. 초기 tactical decision이 나중에 company의 development velocity를 제한할 수 있다.

Strategic programming의 실천 방식은 큰 rewrite보다 작은 습관에 가깝다. 새로운 API를 만들 때 caller가 알아야 할 정보를 줄인다. Class를 추가하기 전에 정말 abstraction이 깊어지는지 묻는다. Exception을 던지기 전에 error를 없앨 수 있는지 생각한다. Comment를 나중에 쓰지 말고 interface를 설계하면서 먼저 쓴다. Existing code를 수정할 때 주변 design까지 조금 개선한다.

이 chapter의 message는 software design의 윤리와도 연결된다. Developer는 “내 task만 끝났다”가 아니라 “다음 사람이 이 code를 더 쉽게 이해하고 바꿀 수 있는가”를 책임져야 한다. Software system은 공동 작업의 산물이고, 좋은 design은 다른 사람의 시간을 존중하는 방식이다.

9. Chapter 4 — Modules Should Be Deep

module은 interface와 implementation을 가진 code unit이다. Class, method, function, package, subsystem, service 모두 module로 볼 수 있다. Modular design의 목표는 developer가 한 순간에 전체 system의 complexity를 모두 마주하지 않게 하는 것이다. 각 module은 외부에 필요한 최소한의 information만 노출하고, 나머지는 내부에 숨긴다.

이 chapter의 핵심 원칙은 module은 deep해야 한다는 것이다. Deep module은 interface는 simple하지만 implementation은 강력하거나 복잡한 기능을 제공한다. Caller는 적은 정보만 알고도 많은 benefit을 얻는다. 예를 들어 balanced tree module이 내부에서 rotation, split, rebalancing을 처리한다면 caller는 insert, delete, lookup 같은 단순한 operation만 알면 된다. 내부 complexity가 크더라도 interface가 단순하면 좋은 module이다.

반대로 shallow module은 interface complexity에 비해 제공하는 benefit이 작다. 어떤 method가 단지 다른 method를 호출하거나, class가 trivial wrapper일 뿐이라면 caller가 새 abstraction을 배워야 하는 비용이 benefit보다 클 수 있다. Shallow module은 system에 이름, file, class, dependency를 추가하지만 cognitive load를 충분히 줄이지 못한다.

Deep module을 판단할 때 중요한 것은 line 수가 아니다. 짧은 code가 나쁜 module일 수도 있고, 긴 code가 좋은 module일 수도 있다. 핵심은 interface가 implementation보다 훨씬 단순한가다. 좋은 module은 내부에 많은 design decision을 감추고, external dependency를 최소화한다. Interface가 작고 stable하면 implementation을 자유롭게 변경할 수 있고, caller는 영향을 받지 않는다.

10. Chapter 4 — Interface, Implementation, Abstraction

Module의 interface는 다른 module의 developer가 그 module을 사용하기 위해 알아야 하는 모든 것이다. 여기에는 formal interface와 informal interface가 모두 포함된다. Formal interface는 method signature, parameter type, return type, public field, thrown exception처럼 language가 어느 정도 검사할 수 있는 정보다. Informal interface는 behavior, constraint, side effect, ordering requirement, performance expectation, semantic rule처럼 code에 명시적으로 표현되지 않는 정보다.

많은 design 문제는 informal interface에서 발생한다. Signature는 단순해 보이는데, 실제로는 “이 method를 부르기 전에 반드시 다른 method를 호출해야 한다”, “특정 flag 조합에서는 undefined behavior다”, “반환된 object는 caller가 수정하면 안 된다” 같은 hidden rule이 있다면 interface는 복잡하다. Good documentation은 이런 informal interface를 드러내야 하지만, 더 좋은 design은 그런 rule 자체를 줄이는 것이다.

abstraction은 복잡한 entity를 단순화한 view다. 좋은 abstraction은 중요한 정보를 보존하고 덜 중요한 정보를 숨긴다. 예를 들어 file abstraction은 storage device의 block layout, caching, scheduling detail을 숨기고 read/write 같은 operation을 제공한다. 그러나 abstraction이 잘못되면 중요한 정보를 숨기거나, 중요하지 않은 세부사항을 노출한다. 그 결과 caller가 system을 잘못 이해하거나 불필요한 complexity를 떠안는다.

Deep module은 좋은 abstraction을 기반으로 한다. Caller가 module을 사용할 때 머릿속에 떠올리는 model이 단순하고 정확해야 한다. Abstraction이 너무 얇으면 caller가 implementation detail까지 알아야 하고, abstraction이 거짓이면 unexpected behavior가 생긴다. 따라서 interface design은 단순히 parameter를 줄이는 작업이 아니라, caller가 어떤 mental model로 system을 이해하게 할지 결정하는 작업이다.

11. Chapter 4 — Shallow Module, Classitis, Java I/O와 Unix I/O

저자는 classitis라는 표현으로, 작은 class를 너무 많이 만드는 경향을 비판한다. Object-oriented programming을 배우면 “각 개념을 class로 나누라”는 조언을 듣기 쉽다. 하지만 class가 많다고 design이 좋아지는 것은 아니다. 각 class는 이름, interface, lifecycle, dependency를 추가한다. Class가 충분한 benefit을 제공하지 않으면 abstraction layer가 오히려 complexity를 증가시킨다.

Java I/O는 shallow abstraction의 사례로 제시된다. Java에서 file input을 하려면 FileInputStream, BufferedInputStream, ObjectInputStream처럼 여러 class를 조합해야 하는 경우가 있다. 각 layer가 조금씩 기능을 추가하지만, caller는 여러 object의 순서와 조합을 알아야 한다. Interface가 넓고 사용법이 복잡해지므로 common case가 단순하지 않다.

반대로 Unix I/O는 상대적으로 deep abstraction으로 평가된다. open, read, write, close 같은 system call은 file, pipe, socket, device 등 다양한 resource에 대해 비슷한 interface를 제공한다. 내부 implementation은 매우 다르지만 caller는 unified abstraction을 사용할 수 있다. 이 interface는 간결하면서도 많은 기능을 제공하므로 깊다.

이 비교의 교훈은 interface가 common case를 쉽게 만들어야 한다는 것이다. Special capability를 제공하더라도, 대부분의 caller가 흔히 하는 작업은 간단해야 한다. API design에서 흔한 실수는 모든 option을 노출해 flexible하게 만들려다가 common usage를 어렵게 만드는 것이다. Deep module은 flexibility를 내부에서 처리하거나 optional하게 만들고, caller에게는 단순한 path를 제공한다.

12. Chapter 5 — Information Hiding

information hiding은 좋은 modular design의 핵심이다. Module은 내부 design decision을 숨기고, 외부 module이 그 decision에 의존하지 않도록 해야 한다. 여기서 숨겨야 하는 information은 단순히 data field만이 아니다. Data representation, algorithm choice, protocol detail, file format, locking policy, cache policy, ordering rule, resource ownership, default behavior 모두 information hiding의 대상이 될 수 있다.

Information hiding이 잘 되면 module 내부 implementation을 바꿔도 외부 code를 수정할 필요가 없다. 예를 들어 text storage module이 내부적으로 line array를 쓰다가 gap buffer나 rope로 바꿔도, external interface가 character range operation을 제공한다면 UI code는 영향을 받지 않는다. Design decision이 localized되어 있기 때문이다.

반대 개념은 information leakage다. 한 design decision이 여러 module에 반영되어 있으면 information이 새고 있는 것이다. 예를 들어 internal data format을 여러 class가 직접 알고 있거나, protocol encoding rule이 parsing module과 business logic module에 동시에 흩어져 있으면 leakage가 생긴다. 이런 경우 decision 하나를 바꾸려면 여러 곳을 수정해야 하며, 빠뜨리는 곳이 생겨 bug가 발생한다.

Information leakage는 code duplication과 다르다. 같은 line이 반복되지 않아도 leakage가 있을 수 있다. 여러 module이 같은 assumption을 공유하지만 그 assumption이 명시적이지 않으면 더 위험하다. 따라서 design review에서는 “이 decision을 누가 알고 있는가?”를 물어야 한다. 좋은 design은 중요한 decision을 가능한 한 한 곳에 둔다.

13. Chapter 5 — Temporal Decomposition과 HTTP Server 예시

temporal decomposition은 code 구조가 information hiding이 아니라 operation의 실행 순서를 따라 나뉘는 현상이다. 예를 들어 어떤 request 처리 과정을 readRequest, parseRequest, validateRequest, processRequest, sendResponse 같은 단계별 class로 나누면 표면적으로는 깔끔해 보일 수 있다. 그러나 각 단계가 같은 data structure와 protocol detail을 공유한다면, 실제로는 information hiding이 실패한 것이다.

Temporal decomposition의 문제는 각 phase가 서로 깊게 연결된다는 점이다. 앞 단계에서 어떤 field를 채우는지, 다음 단계가 어떤 assumption을 갖는지, error case가 어디서 처리되는지 여러 module이 알아야 한다. 이 구조는 procedure를 나눈 것처럼 보이지만, design decision은 여전히 여러 곳에 흩어져 있다.

HTTP server 예시에서 학생들은 request parsing, response generation, parameter handling, default response value 등을 여러 class에 분산시키는 경향을 보인다. 그런데 HTTP protocol detail이나 request parameter rule이 여러 class에 반복되면 change amplification이 발생한다. 새로운 parameter rule이나 default behavior를 추가할 때 여러 module을 수정해야 한다.

더 나은 design은 관련 information을 함께 두는 것이다. 예를 들어 HTTP parameter를 다루는 logic은 parameter abstraction 내부에 모으고, response default는 response object가 스스로 처리하게 만든다. Caller가 매번 header, status, content type, error formatting 같은 detail을 기억하지 않아도 되게 한다. 이때 module 내부 implementation은 조금 더 복잡해질 수 있지만, caller의 interface는 단순해진다.

Temporal decomposition은 workflow를 기준으로 class를 만드는 습관에서 자주 나온다. 좋은 design은 “실행 순서가 무엇인가?”보다 “어떤 information이 함께 변하는가?”, “어떤 decision을 숨길 수 있는가?”, “caller가 몰라도 되는 detail은 무엇인가?”를 기준으로 module boundary를 잡는다.

14. Chapter 5 — Information Hiding을 class 내부에도 적용하기

Information hiding은 module 사이에만 적용되는 원칙이 아니다. Class 내부에서도 method, field, helper function 사이에 같은 문제가 생긴다. 어떤 instance variable이 여러 method에서 직접 조작되고, 각 method가 그 variable의 invariant를 개별적으로 알고 있어야 한다면 class 내부의 design도 복잡하다.

좋은 class는 내부에서도 중요한 invariant를 작은 부분에 localize한다. 예를 들어 buffer의 length, capacity, ownership, alignment 같은 rule이 있다면, 아무 method나 직접 field를 변경하게 두기보다 helper method를 통해 invariant를 유지하게 하는 편이 낫다. Field를 private으로 두는 것만으로 information hiding이 자동으로 달성되지는 않는다. 중요한 것은 representation knowledge가 어디에 퍼져 있는가다.

다만 information hiding을 과도하게 적용하면 또 다른 complexity가 생긴다. 너무 많은 tiny method를 만들어 모든 line을 감추려고 하면 shallow method가 늘어나고 control flow가 흩어진다. 어떤 detail은 module 내부 여러 곳에서 자연스럽게 공유될 수 있다. 핵심은 외부 caller에게 불필요한 information을 요구하지 않는 것이며, 내부에서는 readability와 locality의 balance가 필요하다.

Chapter 5의 red flag는 Information Leakage, Temporal Decomposition, Overexposure다. Overexposure는 API가 common case를 수행하는 caller에게 rarely used feature나 internal option까지 알도록 강요하는 경우다. API가 flexible해 보이지만 대부분의 caller가 불필요한 parameter와 mode를 이해해야 한다면 interface가 shallow해진다.

실전 질문은 다음과 같다. 이 design decision은 몇 곳에 존재하는가? Caller가 이 detail을 알아야 하는가? 같은 assumption이 comment 없이 여러 module에 퍼져 있지는 않은가? Data representation을 바꾸면 어떤 file들이 함께 바뀌는가? 이 질문에 답하기 어렵다면 information hiding이 약한 것이다.

15. Chapter 6 — General-Purpose Modules are Deeper

이 chapter의 핵심은 module을 somewhat general-purpose하게 만들라는 것이다. 여기서 “general-purpose”는 모든 문제를 해결하는 거대한 framework를 만들라는 뜻이 아니다. 현재 필요를 충족하되, 너무 specific한 use case에만 묶이지 않는 interface를 설계하라는 뜻이다.

Special-purpose module은 당장 필요한 feature에는 빠르게 맞을 수 있다. 하지만 interface가 특정 UI action, 특정 workflow, 특정 caller의 assumption에 묶이면 나중에 다른 feature가 생길 때 재사용하기 어렵다. 더 나쁜 경우, special-purpose behavior가 lower-level module 안에 들어가면 higher-level policy와 lower-level mechanism이 섞여 information hiding이 깨진다.

General-purpose module은 interface가 더 단순해지는 경우가 많다. 왜냐하면 특정 caller의 special case를 interface에 반영하지 않고, 더 근본적인 operation을 제공하기 때문이다. 예를 들어 text editor의 text storage class가 “backspace key 처리” 같은 method를 제공하면 UI policy가 storage layer로 내려간다. 반대로 “특정 range의 text를 delete”하는 general operation을 제공하면, UI는 backspace를 그 operation으로 표현할 수 있고 다른 feature도 같은 operation을 사용할 수 있다.

General-purpose design은 abstraction의 깊이를 증가시킨다. Interface는 더 적고 더 본질적인 operation으로 구성되고, implementation은 다양한 use case를 처리한다. Caller는 더 많은 상황에서 같은 interface를 사용할 수 있으므로 learning cost가 줄어든다.

16. Chapter 6 — Text Editor 예시와 API 설계

Text editor의 text storage를 생각해보자. 나쁜 design은 text를 line 단위로 저장한다는 internal representation을 interface에 노출할 수 있다. 예를 들어 insert가 특정 line과 column을 기준으로만 가능하고, newline 처리나 line splitting을 caller가 직접 해야 한다면 UI code가 storage detail을 알아야 한다. 이렇게 되면 internal representation을 바꾸기 어렵고, 여러 caller가 비슷한 logic을 반복한다.

더 나은 API는 character-oriented interface를 제공한다. 예를 들어 arbitrary position에 arbitrary string을 insert하고, arbitrary range를 delete하거나 read할 수 있게 한다. 내부적으로 line array를 쓰든 gap buffer를 쓰든 rope를 쓰든 caller는 character abstraction만 사용한다. 이 방식은 lower-level class가 조금 더 복잡해질 수 있지만, 외부 interface는 훨씬 단순하고 general하다.

General-purpose API를 설계할 때 스스로 물어볼 질문이 있다. 첫째, 이 method가 현재 caller의 special need만 반영하는가, 아니면 더 본질적인 operation인가? 둘째, 이 API를 사용하면 caller가 internal representation을 알 필요가 줄어드는가? 셋째, 미래의 비슷한 feature도 이 interface로 표현할 수 있는가? 넷째, general하게 만든다고 해서 common case가 더 어려워지지는 않는가?

중요한 것은 적절한 generality다. 너무 general한 API는 모든 가능성을 열어두느라 복잡해진다. 예를 들어 수십 개의 callback, policy object, mode flag, configuration parameter가 필요하다면 generality가 아니라 overengineering일 수 있다. 저자가 말하는 general-purpose는 “현재 필요보다 약간 더 넓고, abstraction 관점에서 자연스러운” 정도다.

17. Chapter 6 — Specialization을 위로 밀어 올리기

저자는 special-purpose logic을 가능한 한 higher layer로 밀어 올리라고 말한다. Lower layer는 mechanism을 제공하고, higher layer는 policy를 결정하는 편이 좋다. Text storage class는 text range를 조작하는 mechanism을 제공하고, UI layer가 backspace, selection, undo command 같은 policy를 표현한다.

하지만 “위로”만이 답은 아니다. 어떤 complexity는 아래로 끌어내려야 한다. Chapter 8의 Pull Complexity Downwards와 연결된다. 기준은 caller가 더 잘 처리할 수 있는가, module 내부가 더 잘 처리할 수 있는가다. Special-purpose policy가 caller context에 의존한다면 위로 올린다. 반대로 많은 caller가 반복해서 처리해야 하는 공통 detail이면 아래 module이 맡는다.

Editor의 undo mechanism 예시도 이 관점을 보여준다. Undo를 구현하려면 text modification의 history를 기록해야 한다. 이 logic을 UI action마다 흩어놓으면 duplicate와 inconsistency가 생긴다. 더 나은 design은 text modification operation과 undo record generation을 적절히 연결해, caller가 매번 history detail을 처리하지 않도록 하는 것이다. 다만 undo policy가 UI command semantics와 강하게 연결되어 있다면 그 부분은 higher layer에 남겨야 한다.

General-purpose module은 special case를 줄이는 데도 도움이 된다. Special case는 code path를 늘리고 cognitive load를 높인다. API가 본질적인 operation을 제공하면 많은 special behavior를 같은 mechanism으로 표현할 수 있다. 예를 들어 “마지막 line 삭제”, “selection 삭제”, “backspace”가 모두 range deletion으로 표현될 수 있다면 code는 단순해진다.

18. Chapter 7 — Different Layer, Different Abstraction

이 chapter의 원칙은 단순하다. 서로 다른 layer는 서로 다른 abstraction을 가져야 한다. Layer를 추가했는데 위 layer와 아래 layer가 거의 같은 interface를 갖고 같은 일을 한다면, 그 layer는 complexity만 추가했을 가능성이 높다.

대표적 red flag는 pass-through method다. 어떤 method가 argument를 거의 그대로 받아 다른 object의 비슷한 method에 넘기기만 한다면, caller는 새 method 이름과 class를 배워야 하지만 실제 benefit은 거의 없다. Pass-through method가 많으면 class hierarchy가 두꺼워 보이지만, abstraction은 깊어지지 않는다.

물론 같은 signature를 가진 method가 항상 나쁜 것은 아니다. dispatcher는 URL, command, message type 등을 보고 적절한 handler를 선택한 뒤 argument를 넘길 수 있다. 이 경우 dispatcher는 routing이라는 distinct functionality를 제공한다. Interface inheritance처럼 동일한 interface를 여러 implementation이 공유하는 것도 유용하다. 문제는 새로운 layer가 어떤 새로운 abstraction이나 decision을 제공하지 않을 때다.

decorator 또는 wrapper pattern도 조심해야 한다. Decorator는 기존 object를 감싸서 기능을 확장하지만, API 대부분을 그대로 반복하는 경우가 많다. 그러면 boilerplate가 많아지고 shallow class가 늘어난다. Decorator가 필요한 경우도 있다. 예를 들어 외부 library interface를 application의 interface에 맞추거나, 기존 class를 수정할 수 없을 때다. 그러나 새 기능이 underlying class와 자연스럽게 연결되어 있고 많은 caller가 사용할 것이라면, 별도 decorator보다 core class에 넣는 것이 더 나을 수 있다.

19. Chapter 7 — Interface와 Implementation은 달라야 한다

“Different layer, different abstraction” 원칙은 class interface와 implementation 사이에도 적용된다. 좋은 class는 내부 representation을 그대로 interface에 드러내지 않는다. 내부가 line array라고 해서 interface도 line-oriented일 필요는 없다. 내부가 hash table이라고 해서 caller가 bucket이나 collision을 알아야 하는 것도 아니다.

Interface는 사용자가 원하는 conceptual operation을 기준으로 설계해야 한다. Implementation은 그 operation을 효율적으로 수행하기 위한 세부 구조다. 두 세계가 너무 비슷하면 implementation detail이 interface로 새고 있는 것이다. 그러면 representation을 바꾸기 어려워지고, caller가 내부 detail에 의존하게 된다.

pass-through variable도 중요한 red flag다. 어떤 variable이 여러 method call chain을 따라 계속 전달되지만 중간 method들은 그 variable을 사용하지 않는다면, system 구조가 잘못되었을 수 있다. 중간 method들은 불필요한 parameter를 알아야 하고, 새 variable이 추가될 때 call chain 전체가 바뀐다.

Pass-through variable을 줄이는 방법으로 shared object나 context object가 제시된다. Context object는 application-wide state를 한 곳에 모아 필요할 때 접근하게 한다. 그러나 context는 global variable과 비슷한 위험을 가진다. 무엇이 들어 있는지, 누가 사용하는지, lifecycle이 어떤지 불명확해질 수 있다. 따라서 context는 discipline이 필요하다. 필요한 state를 아무렇게나 넣는 쓰레기통이 되면 또 다른 complexity source가 된다.

이 chapter의 결론은 design infrastructure도 비용이라는 것이다. Interface, argument, function, class, definition 하나하나가 developer가 배워야 할 대상이다. 어떤 element가 complexity를 줄이려면, 그것이 추가하는 learning cost보다 더 큰 benefit을 제공해야 한다.

20. Chapter 8 — Pull Complexity Downwards

이 chapter의 원칙은 좋은 API designer에게 특히 중요하다. 어쩔 수 없는 complexity가 있다면, 가능하면 module 내부로 끌어내려라. Module을 만든 사람 한 명이 조금 더 고생해서 많은 caller가 편해진다면 전체 system complexity는 줄어든다.

Developer는 종종 반대로 행동한다. 어려운 상황을 만나면 exception을 던지고 caller에게 맡긴다. Configuration parameter를 추가해 user나 administrator에게 decision을 넘긴다. “이 경우는 caller가 알아서 처리하게 하자”고 생각한다. 단기적으로는 module author의 일이 줄어든다. 하지만 caller가 많아지면 같은 complexity가 여러 곳에 복제된다. System 전체 관점에서는 더 비싸다.

Text editor text class 예시에서 character-oriented interface는 complexity를 downward로 끌어내린다. UI code가 line splitting, merging, indexing detail을 처리하지 않아도 된다. Text class 내부는 더 어려워지지만, 많은 caller가 훨씬 단순해진다. 이것이 deep module의 본질이다.

Configuration parameter도 비슷하다. Parameter를 노출하면 flexible해 보이지만, user가 올바른 값을 판단하기 어렵다면 complexity를 위로 밀어 올린 것이다. Cache size, thread count, timeout, retry policy 같은 값은 workload와 internal implementation을 알아야 적절히 정할 수 있다. User가 더 잘 알 수 없다면 module이 sensible default나 adaptive policy를 제공하는 편이 낫다.

21. Chapter 8 — Complexity를 아래로 내릴 때의 한계

Pull Complexity Downwards는 무조건 모든 기능을 lower-level module에 넣으라는 뜻이 아니다. 그렇게 하면 giant class가 생기고, module 내부 complexity가 감당할 수 없게 된다. 핵심은 누가 그 complexity를 가장 자연스럽게 처리할 수 있는가다.

Lower-level module이 information을 이미 가지고 있고, 여러 caller가 같은 logic을 반복해야 한다면 아래로 내리는 것이 좋다. 반대로 higher-level policy나 user interaction semantics에 의존하는 decision이라면 위에 남겨야 한다. Text storage class가 arbitrary range deletion을 제공하는 것은 좋지만, “backspace key의 의미”를 직접 구현하는 것은 UI policy를 storage layer에 섞는 것이다.

좋은 design은 complexity의 위치를 조정한다. Complexity를 없앨 수 없다면 가장 적은 곳에, 가장 관련 information이 많은 곳에, 가장 많은 caller를 단순하게 만드는 곳에 둔다. 이 원칙은 API, kernel interface, distributed system protocol, storage engine, compiler pass 등 모든 software layer에 적용된다.

실전에서 이 원칙을 적용하려면 code review에서 다음 질문을 던질 수 있다. 이 exception은 정말 caller가 처리해야 하는가? 이 configuration은 user가 우리보다 더 잘 판단할 수 있는가? 여러 caller가 같은 validation을 반복하고 있지는 않은가? Common case를 위해 caller가 rare case detail을 알아야 하지는 않는가? Module 내부가 조금 더 수고하면 external interface가 크게 단순해지는가?

22. Chapter 9 — Better Together Or Better Apart?

Software design에서 가장 흔한 질문 중 하나는 “이 code를 합쳐야 하는가, 나눠야 하는가?”다. 저자는 작은 component로 나누는 것이 항상 좋은 것은 아니라고 말한다. 분리는 independence를 높일 수 있지만, 잘못된 분리는 dependency와 boilerplate를 늘린다. 반대로 합치기는 duplication을 줄일 수 있지만, unrelated concern을 섞으면 module이 비대해진다.

함께 두는 것이 좋은 경우는 첫째, information이 공유될 때다. 두 piece of code가 같은 data structure, invariant, design decision을 깊게 공유한다면 따로 두면 information leakage가 생길 수 있다. 함께 두면 그 information을 localize할 수 있다.

둘째, 함께 두면 interface가 단순해질 때다. 두 class가 분리되어 있어 caller가 둘 사이의 ordering, coordination, lifecycle을 직접 관리해야 한다면 caller interface가 복잡해진다. 하나의 module이 coordination을 내부에서 처리하면 caller는 더 단순한 abstraction을 사용할 수 있다.

셋째, 함께 두면 duplication이 제거될 때다. Nontrivial logic이 여러 곳에서 반복되면 change amplification이 생긴다. 단순한 한두 줄의 duplication은 괜찮을 수 있지만, policy나 invariant가 반복되면 design smell이다.

그러나 모든 것을 합쳐서는 안 된다. General-purpose code와 special-purpose code는 분리하는 것이 좋다. General mechanism이 specific policy에 오염되면 재사용성이 떨어지고 interface가 복잡해진다. Chapter 6의 원칙과 연결된다. Mechanism은 lower-level module에 두고, special policy는 higher-level module에 둔다.

23. Chapter 9 — Cursor/Selection, Logging, Method Split/Join

Editor에서 insertion cursor와 selection은 함께 둘지 나눌지 고민할 수 있는 좋은 예다. Cursor는 현재 삽입 위치를 나타내고, selection은 선택된 text range를 나타낸다. 겉으로는 다른 개념처럼 보이지만, 둘은 UI behavior에서 밀접하게 연결된다. Selection이 있으면 cursor 위치가 selection과 관련되고, text insertion은 selection을 대체할 수 있다. 같은 state와 invariant를 공유한다면 분리보다 통합이 더 단순할 수 있다.

Logging class 예시는 반대 방향을 보여준다. Logging은 여러 module에서 사용할 수 있는 general-purpose service다. 특정 feature logic과 섞이면 재사용성이 떨어지고 caller가 logging detail에 노출될 수 있다. 이 경우 별도 module로 분리하는 것이 자연스럽다. 즉, “함께” 또는 “따로”의 답은 개념이 얼마나 독립적인지, information이 얼마나 공유되는지, interface가 어떻게 바뀌는지에 따라 결정된다.

Method를 split하거나 join할 때도 같은 기준을 적용한다. Method가 너무 길면 이해하기 어렵지만, 너무 많이 쪼개면 control flow를 따라가기 어렵고 pass-through method가 생긴다. Method를 쪼개는 좋은 이유는 독립적인 subtask가 있고, 그 subtask가 명확한 abstraction을 형성하며, 이름이 code보다 더 의미를 잘 전달할 때다. 나쁜 이유는 단순히 line 수를 줄이기 위해서다.

저자는 Clean Code류의 “아주 짧은 method가 좋다”는 관점과 다른 입장을 보인다. 짧은 method 자체가 목적이 아니다. 중요한 것은 cognitive load와 abstraction depth다. 긴 method라도 구조가 명확하고 한 곳에서 읽는 것이 더 쉬울 수 있다. 반대로 짧은 method가 많으면 reader는 call graph를 계속 이동해야 한다.

24. Chapter 9 — Splitting과 Joining의 기준

Code를 분리할 때는 다음 기준이 도움이 된다. 첫째, 분리된 piece가 독립적으로 이해될 수 있는가? 둘째, 분리 후 interface가 충분히 단순한가? 셋째, 분리로 인해 caller가 coordination을 더 많이 해야 하지는 않는가? 넷째, 분리된 module이 information hiding을 개선하는가?

Code를 합칠 때도 기준이 필요하다. 첫째, 두 piece가 같은 information을 공유하는가? 둘째, 따로 둔 결과 duplicate logic이나 repeated check가 생겼는가? 셋째, 두 method를 따로 이해하기 어렵고 항상 함께 봐야 하는가? 이런 경우 Conjoined Methods red flag가 발생한다. Conjoined Methods는 두 method가 너무 많은 dependency를 공유해서 하나만 읽고는 이해할 수 없는 상태다. 이름상으로는 분리되어 있지만 실제 mental model은 하나다.

좋은 design은 module boundary가 자연스러운 conceptual boundary와 맞는다. Boundary는 단지 file이나 class를 나누는 선이 아니라, knowledge를 나누는 선이다. 한 boundary를 넘을 때마다 developer는 interface만 알면 되고 implementation detail은 몰라도 되어야 한다. Boundary를 넘을 때마다 오히려 더 많은 shared assumption을 알아야 한다면 잘못된 boundary다.

이 chapter는 “작게 나누기”와 “크게 합치기” 사이의 단순한 선택을 넘어선다. 핵심은 complexity의 총량이다. 분리로 인해 local complexity가 줄어도 global coordination complexity가 증가하면 좋지 않다. 합치기로 인해 duplication이 줄어도 module이 unrelated responsibility를 떠안으면 좋지 않다. 좋은 designer는 이 tradeoff를 case by case로 판단한다.

25. Chapter 10 — Define Errors Out Of Existence

Exception handling은 software complexity의 큰 source다. Exception은 normal control flow를 깨고, handler는 보통 드물게 실행되며, test하기 어렵고, error path에서 또 다른 error가 발생할 수 있다. 특히 distributed system이나 I/O heavy system에서는 exceptional condition이 많다. Network packet loss, timeout, server crash, partial write, invalid input, resource exhaustion 등은 모두 code를 복잡하게 만든다.

저자는 모든 error를 무시하라고 말하지 않는다. 오히려 error handling이 중요하기 때문에, unnecessary exception을 줄여야 한다고 말한다. Exception을 던지는 것은 쉽지만, exception을 올바르게 처리하는 것은 어렵다. API가 많은 exception을 노출하면 caller는 각 exception의 의미와 처리 방법을 알아야 한다. 따라서 exception도 interface의 일부이며, exception이 많을수록 interface는 복잡해진다.

이 chapter의 핵심 기법은 define errors out of existence다. Error를 발견한 뒤 caller에게 던지는 대신, API definition을 바꿔 그 상황을 error가 아닌 정상 behavior로 만들 수 있는지 생각한다. 예를 들어 어떤 variable을 삭제하는 command가 variable이 없을 때 error를 던지는 대신, “존재하지 않으면 아무 일도 하지 않는다”로 정의할 수 있다. Caller가 원하는 것은 결국 variable이 없는 상태이므로, 이미 없으면 성공으로 볼 수 있다.

이 approach는 bug를 숨기는 것이 아니라 API를 더 자연스럽게 정의하는 것이다. Caller가 보통 원하는 postcondition이 무엇인지 생각한다. Operation이 그 postcondition을 만족한다면, intermediate condition을 error로 만들 필요가 없을 수 있다.

26. Chapter 10 — Unix File Deletion과 Java Substring

File deletion 예시는 design definition의 힘을 보여준다. Windows는 process가 file을 열고 있으면 delete를 허용하지 않는 경우가 있어 user와 developer를 불편하게 만든다. Caller는 file이 사용 중인지, 누가 열었는지, 언제 다시 시도해야 하는지 처리해야 한다.

Unix는 다른 semantics를 사용한다. Open된 file을 unlink하면 directory entry는 제거되고, 실제 file content는 마지막 file descriptor가 닫힐 때 사라진다. Delete operation은 성공하고, 이미 open한 process는 계속 file을 사용할 수 있다. 이 semantics는 “open file delete”라는 error class를 크게 줄인다. Caller는 복잡한 retry logic을 덜 필요로 한다.

Java String.substring 예시도 나온다. 기존 API는 index가 range 밖이면 exception을 던진다. 저자는 alternative definition을 상상한다. 요청한 range와 실제 string range의 intersection을 반환하고, intersection이 비면 empty string을 반환하도록 정의할 수 있다. 이렇게 하면 caller가 boundary check를 반복하지 않아도 된다. 물론 모든 상황에서 이런 semantics가 옳은 것은 아니다. 그러나 API가 caller의 common intent를 더 잘 반영한다면 error case를 normal case로 흡수할 수 있다.

이 접근에 대한 반론은 “exception이 bug를 잡아주지 않느냐”다. 저자의 답은 단순함이 bug를 줄이는 가장 좋은 방법이라는 것이다. Error를 많이 던진다고 bug가 줄어드는 것은 아니다. Caller가 복잡한 handler를 잘못 작성하면 오히려 bug가 늘어난다. 어떤 condition이 진짜 programming error인지, 아니면 API가 자연스럽게 처리할 수 있는 boundary condition인지 구분해야 한다.

27. Chapter 10 — Mask Exceptions와 Exception Aggregation

exception masking은 lower-level에서 exception을 처리해 higher-level이 알 필요 없게 만드는 기법이다. 예를 들어 TCP는 packet loss, retransmission, ordering 문제를 application에게 숨긴다. Application은 reliable byte stream abstraction을 사용하고, 대부분의 packet-level error를 직접 처리하지 않는다. 이처럼 lower layer가 error를 mask하면 upper layer interface가 deep해진다.

NFS 예시는 논쟁적이지만 흥미롭다. NFS server가 일시적으로 응답하지 않으면 client operation이 hang될 수 있다. User는 불편할 수 있지만, application이 partial failure를 직접 처리하도록 exception을 던지면 훨씬 많은 application code가 복잡해진다. Server가 돌아오면 operation이 계속되는 semantics는 많은 error handling을 줄인다. 물론 timeout이 더 적절한 domain도 있다. 핵심은 caller에게 error를 던지는 것이 항상 친절한 것이 아니라는 점이다.

exception aggregation은 여러 exception을 한 곳에서 처리하는 기법이다. Web server에서 각 URL handler가 parameter missing exception을 따로 처리하면 handler마다 boilerplate가 생긴다. 더 나은 방식은 handler에서 exception을 propagate하고, top-level dispatch layer가 공통 error response를 생성하는 것이다. 이렇게 하면 error handling policy가 한 곳에 모이고, individual service logic은 normal case에 집중한다.

Exception aggregation은 error type을 줄이는 것과도 연결된다. Caller가 구분해서 다르게 처리하지 않을 exception을 여러 종류로 나눠 던질 필요가 없다. 구분이 meaningful하지 않으면 하나의 failure path로 묶는 편이 단순하다. Error information은 logging이나 debugging용으로 보존할 수 있지만, interface 차원에서 caller에게 불필요한 branch를 강요하지 않는다.

28. Chapter 10 — Just Crash?와 Error Handling의 절제

어떤 error는 처리할 수 없다. Internal invariant violation, impossible state, memory corruption, critical assertion failure처럼 system이 더 이상 정상적으로 동작한다고 믿을 수 없는 경우에는 crash가 더 정직한 선택일 수 있다. 특히 fail-stop behavior가 data corruption보다 나을 수 있다.

하지만 “그냥 crash”도 남용하면 안 된다. User input validation, network failure, resource shortage처럼 expected condition을 crash로 처리하면 system reliability가 떨어진다. 기준은 recovery가 의미 있는가, caller가 합리적으로 처리할 수 있는가, 그리고 error를 normal semantics로 흡수할 수 있는가다.

Chapter 10의 큰 message는 exception handling을 design의 일부로 보라는 것이다. 많은 codebase에서 error handling은 나중에 덧붙이는 부가 작업처럼 취급된다. 하지만 exception은 interface를 넓히고 control flow를 복잡하게 만든다. 따라서 API를 설계할 때 normal behavior뿐 아니라 error behavior도 함께 설계해야 한다.

실전 checklist는 다음과 같다. 이 exception은 정말 필요한가? Caller가 이 exception을 보고 meaningful하게 다른 action을 할 수 있는가? Operation의 postcondition을 바꿔 error를 없앨 수 있는가? Lower layer가 retry, default, cleanup, normalization을 통해 mask할 수 있는가? 여러 handler를 하나로 aggregate할 수 있는가? Error path가 normal path보다 더 복잡해지지는 않았는가?

29. Chapter 11 — Design it Twice

좋은 design은 처음 떠오른 idea에서 바로 나오지 않는 경우가 많다. 저자는 중요한 design decision을 할 때 최소한 두 가지 이상의 alternative를 만들어 비교하라고 말한다. 이것이 Design it Twice다. 실제로는 두 번보다 더 많이 생각할 수도 있지만, 최소 두 개의 option을 만드는 습관이 중요하다.

첫 번째 idea는 보통 현재 익숙한 pattern이나 가장 obvious한 solution이다. 그것이 나쁘다는 뜻은 아니다. 하지만 alternative를 만들지 않으면 tradeoff를 볼 수 없다. 두 번째 design을 고민하는 과정에서 어떤 information을 숨길 수 있는지, interface를 어떻게 줄일 수 있는지, general-purpose abstraction이 가능한지, special case를 없앨 수 있는지 더 잘 보인다.

Design alternative를 비교할 때는 단순히 “어느 것이 구현하기 쉬운가”만 보면 안 된다. Interface complexity, implementation complexity, performance, extensibility, dependency, testability, error handling, common case simplicity를 함께 봐야 한다. 때로는 implementation이 조금 더 어려운 design이 interface를 크게 단순화해 전체 system에는 더 좋다.

Design it Twice는 시간 낭비처럼 보일 수 있지만, 실제로는 큰 mistake를 초기에 발견하는 cheap한 방법이다. Code를 많이 작성한 뒤 design을 바꾸는 것보다, whiteboard나 comment-level에서 alternative를 비교하는 것이 훨씬 싸다. 특히 public API, storage layout, protocol, concurrency model, module boundary처럼 나중에 바꾸기 어려운 decision에서는 더욱 중요하다.

30. Chapter 11 — Alternative를 만드는 방법

Alternative를 만들 때는 단순히 변수명만 바꾸지 말고, 다른 abstraction boundary를 시도해야 한다. 예를 들어 request handling system을 설계한다면, URL별 handler class를 둘 수도 있고, declarative route table을 둘 수도 있고, middleware pipeline을 둘 수도 있고, request object가 parameter parsing을 책임지게 할 수도 있다. 각 design은 information hiding의 위치와 error handling 방식이 다르다.

좋은 비교 질문은 다음과 같다. Caller가 알아야 하는 정보는 어느 design이 더 적은가? Common case는 어느 design이 더 짧고 명확한가? Rare case가 common path를 오염시키는가? Internal representation을 바꿀 때 영향을 받는 module은 어디까지인가? Testing은 어느 쪽이 쉬운가? Performance critical path에 unnecessary layer가 들어가는가?

Design it Twice는 경험을 빠르게 늘리는 훈련이기도 하다. Alternative를 만들고 비교하면 design space를 보는 눈이 생긴다. 처음에는 두 번째 design이 어색할 수 있지만, 시간이 지나면 첫 번째 idea의 약점을 더 빨리 발견하게 된다. 좋은 designer는 하나의 solution에 집착하지 않고, problem을 다른 abstraction으로 재표현할 수 있다.

중요한 것은 완벽한 design을 찾느라 끝없이 미루지 않는 것이다. Design it Twice는 paralysis를 위한 기법이 아니라, 좋은 enough design을 더 자신 있게 선택하기 위한 기법이다. 두세 가지 option을 비교한 뒤 가장 단순하고 깊은 abstraction을 고르고, implementation 과정에서 새로 발견되는 문제를 다시 design에 반영하면 된다.

31. Chapter 12 — Why Write Comments? The Four Excuses

저자는 comment에 대한 흔한 변명을 네 가지로 정리한다. 첫째, “good code is self-documenting”이라는 주장이다. 물론 좋은 naming과 clear structure는 중요하다. 그러나 code만으로는 모든 것을 설명할 수 없다. Code는 what과 how를 어느 정도 보여주지만, why, design rationale, abstraction, constraint, tradeoff는 잘 드러내지 못한다.

둘째, “comment 쓸 시간이 없다”는 변명이다. 저자는 comment가 시간을 낭비하는 것이 아니라 design과 maintenance 비용을 줄이는 investment라고 본다. 특히 interface comment는 caller가 module을 올바르게 사용하도록 돕고, implementation comment는 reader가 nonobvious logic을 이해하게 한다. Comment가 없어서 code를 다시 reverse engineering하는 비용은 훨씬 크다.

셋째, “comment는 금방 outdated되어 misleading해진다”는 걱정이다. 이 문제는 실제로 존재한다. 하지만 해결책은 comment를 없애는 것이 아니라, 좋은 comment를 쓰고 code 근처에 두며, code 변경 시 함께 update하는 discipline을 갖는 것이다. 나쁜 comment가 있을 수 있다는 이유로 모든 comment를 포기하면 더 큰 obscurity가 생긴다.

넷째, “내가 본 comment는 다 쓸모없었다”는 주장이다. 많은 comment가 code를 반복하거나 obvious한 정보를 말하는 것은 맞다. 그러나 그것은 comment라는 도구가 나쁜 것이 아니라 잘못 사용된 것이다. 좋은 comment는 code에서 obvious하지 않은 정보를 제공한다. 특히 interface, invariant, side effect, concurrency requirement, design rationale은 comment가 없으면 추측하기 어렵다.

32. Chapter 12 — Comment의 benefit

Well-written comment는 여러 benefit을 제공한다. 첫째, developer가 code를 빠르게 이해하게 한다. Code를 한 줄씩 추적하지 않아도 module의 목적, interface contract, important invariant를 알 수 있다. 둘째, comment는 design tool이다. Interface comment를 먼저 쓰면, API가 caller 관점에서 자연스러운지 확인할 수 있다. Comment로 설명하기 어려운 interface는 design 자체가 복잡할 가능성이 크다.

셋째, comment는 abstraction을 강화한다. 좋은 interface comment는 implementation detail을 숨기고, caller가 가져야 할 mental model을 제공한다. Implementation comment는 low-level code block이 어떤 higher-level purpose를 수행하는지 알려준다. 이렇게 comment는 code의 다른 abstraction level을 연결한다.

넷째, comment는 collaboration을 돕는다. Codebase는 여러 사람이 읽고 수정한다. Comment는 original author의 design intent를 남겨, future developer가 잘못된 refactoring을 하지 않게 한다. 특히 cross-module design decision은 code 한 곳만 봐서는 알 수 없으므로 comment나 design note가 필요하다.

저자는 “comments are failures”라는 의견도 다룬다. 어떤 사람들은 comment가 필요하다면 code가 충분히 clear하지 않은 것이라고 말한다. 이 말은 일부 상황에서 맞다. Comment로 복잡한 code를 덮으려 하기보다 code를 단순하게 고쳐야 할 때가 있다. 그러나 code만으로 표현할 수 없는 정보도 많다. 따라서 좋은 목표는 comment를 없애는 것이 아니라, code와 comment가 서로 다른 정보를 제공하게 하는 것이다.

33. Chapter 13 — Comments Should Describe Things Not Obvious from the Code

이 chapter의 핵심 원칙은 명확하다. Comment는 code에서 obvious하지 않은 것을 설명해야 한다. Code 옆에 code를 그대로 반복하는 comment는 value가 없다. 예를 들어 // increment i 같은 comment는 reader의 시간을 낭비한다. 좋은 comment는 왜 이 code가 필요한지, 이 block이 어떤 role을 하는지, caller가 어떤 contract를 알아야 하는지 설명한다.

Comment는 abstraction level에 따라 역할이 다르다. Lower-level comment는 precision을 제공한다. 예를 들어 특정 variable의 unit, boundary condition, lock ownership, memory alignment requirement, error code 의미 같은 detail은 정확히 써야 한다. Higher-level comment는 intuition을 제공한다. Module 전체가 어떤 abstraction을 제공하는지, 왜 이런 algorithm을 선택했는지, 어떤 tradeoff가 있는지 알려준다.

좋은 comment를 쓰려면 convention이 필요하다. 어떤 comment가 interface documentation인지, 어떤 comment가 implementation note인지, 어떤 위치에 어떤 형식으로 쓰는지 팀 차원의 consistency가 있어야 한다. Convention이 없으면 comment가 빠지거나 중복되고, reader는 어디에서 어떤 정보를 찾아야 하는지 모른다.

“Don’t repeat the code”는 이 chapter의 반복되는 조언이다. Comment가 code와 같은 abstraction level에서 같은 내용을 말하면 redundant하다. 대신 code보다 higher level에서 의미를 설명하거나, code보다 lower level에서 precise constraint를 설명해야 한다. 즉, comment는 code와 다른 관점을 제공해야 한다.

34. Chapter 13 — Interface Documentation

Interface comment는 module 사용자에게 필요한 contract를 설명한다. Method comment라면 method가 무엇을 하는지, parameter의 의미가 무엇인지, return value가 무엇인지, side effect는 무엇인지, caller가 지켜야 할 precondition은 무엇인지, exception이나 error behavior는 무엇인지 써야 한다. 중요한 것은 implementation detail을 섞지 않는 것이다.

Interface documentation은 caller가 implementation을 읽지 않고도 module을 사용할 수 있게 해야 한다. Caller가 source code 내부를 뒤져야 한다면 interface comment가 실패한 것이다. 특히 library, public API, kernel API, RPC endpoint, protocol handler처럼 여러 사람이 사용하는 interface는 comment가 design의 일부다.

좋은 interface comment는 abstraction을 설명한다. 예를 들어 “이 method는 internal array의 element를 복사한다”보다 “이 method는 logical sequence의 specified range를 반환한다”가 더 interface답다. Internal array는 implementation detail이고, logical sequence는 caller abstraction이다. Interface comment에 implementation detail이 들어가면 caller가 그 detail에 의존하게 될 수 있다.

Red flag는 Implementation Documentation Contaminates Interface다. Interface comment가 사용자에게 필요 없는 implementation detail을 설명하면 abstraction boundary가 흐려진다. 물론 performance characteristic이나 ordering guarantee처럼 caller에게 중요한 implementation-derived property는 interface에 포함될 수 있다. 기준은 caller가 올바르게 사용하기 위해 그 정보를 알아야 하는가다.

35. Chapter 13 — Implementation Comment: What and Why, Not How

Implementation comment는 code 내부의 reader를 돕는다. 여기서도 단순히 code가 어떻게 실행되는지 line by line으로 설명하면 안 된다. Code 자체가 how를 보여준다. Comment는 이 block이 무엇을 달성하려는지, 왜 이런 approach를 택했는지, 어떤 invariant를 유지하는지 설명해야 한다.

예를 들어 복잡한 loop가 있다면 comment는 “i를 증가시키며 array를 순회한다”가 아니라 “free list에서 충분히 큰 contiguous region을 찾는다”처럼 higher-level purpose를 말해야 한다. Algorithm이 nonobvious한 이유가 있다면 그 이유도 써야 한다. “이 branch는 rare하지만, crash recovery 중 duplicate request를 처리하기 위해 필요하다” 같은 comment는 reader에게 중요한 context를 제공한다.

Implementation comment는 especially important한 곳이 있다. 첫째, tricky code나 optimized code다. Performance를 위해 straightforward code를 포기했다면 rationale을 남겨야 한다. 둘째, concurrency code다. Lock ordering, memory visibility, race avoidance는 code만 보고 추론하기 어렵다. 셋째, error handling code다. Rare path는 test와 runtime 경험이 적어 comment가 더 중요하다.

하지만 comment로 나쁜 code를 정당화하면 안 된다. Comment가 길어지고 복잡해진다면 design이 잘못된 것일 수 있다. Hard to Describe red flag는 어떤 variable이나 method를 정확히 설명하려면 comment가 너무 길어지는 경우다. 이럴 때는 comment를 더 길게 쓰기보다 abstraction을 다시 설계해야 한다.

36. Chapter 13 — Cross-Module Design Decisions

어떤 design decision은 한 file 안에 자연스럽게 위치하지 않는다. 예를 들어 protocol semantics, global locking order, data replication strategy, transaction invariant, request lifecycle 같은 decision은 여러 module에 걸쳐 영향을 준다. 이런 decision은 code만으로 발견하기 어렵기 때문에 명시적 documentation이 필요하다.

Cross-module decision을 documentation하지 않으면 unknown unknowns가 생긴다. Developer가 한 module을 수정하면서 다른 module의 hidden assumption을 깨뜨릴 수 있다. 예를 들어 cache invalidation rule이 storage module, network handler, UI refresh code에 흩어져 있는데 어디에도 전체 rule이 설명되어 있지 않다면 maintenance가 위험해진다.

이런 comment는 반드시 거대한 design document일 필요는 없다. 적절한 central location에 short design note를 두고, 관련 code에서 그 note를 참조할 수 있다. 중요한 것은 decision의 existence와 rationale이 discoverable해야 한다는 점이다. Reader가 “왜 이렇게 되어 있지?”라고 물었을 때 답을 찾을 수 있어야 한다.

Comment 작성은 design review와 연결된다. Interface comment를 읽었는데 behavior가 모호하다면 interface가 모호한 것이다. Implementation comment를 쓰려는데 explanation이 복잡하다면 code structure가 복잡한 것이다. Cross-module comment가 너무 많다면 module boundary가 잘못되었을 수 있다. 좋은 comment는 design 문제를 드러내는 diagnostic tool이기도 하다.

37. Chapter 14 — Choosing Names

Naming은 code obviousness에 직접 영향을 준다. 좋은 name은 reader의 첫 추측을 맞게 만든다. 나쁜 name은 reader가 잘못된 mental model을 갖게 하거나, name을 보고도 아무 정보를 얻지 못하게 한다. 이 chapter의 핵심은 name은 precise해야 한다는 것이다.

Vague name은 대표적 red flag다. data, info, object, manager, handle, process, doIt 같은 이름은 context 없이는 거의 의미가 없다. 물론 어떤 domain에서는 handle이나 manager가 정확한 의미를 가질 수 있지만, 대부분은 책임을 흐리게 만든다. Name이 모호하면 reader는 implementation을 읽어야 하고 cognitive load가 증가한다.

좋은 name은 entity의 가장 중요한 property를 담는다. Variable이라면 무엇을 저장하는지뿐 아니라 unit, state, ownership, range, mutability를 드러낼 수 있다. 예를 들어 timeout보다 timeoutMs가 더 precise하고, length보다 numBytes 또는 numChars가 더 precise할 수 있다. Boolean name은 특히 조심해야 한다. active보다 isConnectionOpen, hasPendingWrites, shouldRetry처럼 condition을 명확히 하는 편이 좋다.

Name은 consistent해야 한다. 같은 개념에는 같은 단어를 쓰고, 다른 개념에는 다른 단어를 써야 한다. 한 곳에서는 block, 다른 곳에서는 chunk, 또 다른 곳에서는 segment라고 부르면 reader는 셋이 같은지 다른지 고민해야 한다. Consistency는 documentation보다 강력한 communication tool이다.

38. Chapter 14 — Extra Words와 Hard to Pick Name

좋은 name은 precise해야 하지만, 불필요하게 길어서는 안 된다. extra words는 의미를 추가하지 않고 noise만 늘린다. 예를 들어 class 안의 method에 class name을 반복하거나, type information이 이미 obvious한데 name에 다시 넣는 경우가 있다. Name의 길이는 그 name이 추가하는 information과 균형을 이뤄야 한다.

짧은 name이 항상 나쁜 것은 아니다. Scope가 매우 좁은 loop index나 local variable은 짧아도 된다. 반대로 넓은 scope에서 오래 살아남는 variable, public method, class, module name은 더 descriptive해야 한다. Name의 precision은 사용 범위와 중요도에 비례해야 한다.

Hard to Pick Name은 중요한 red flag다. 어떤 entity에 좋은 name을 붙이기 어렵다면, 그 entity의 responsibility가 불분명하거나 여러 개념이 섞여 있을 수 있다. 이름을 고민하는 과정은 design 문제를 발견하는 기회다. 적절한 name이 떠오르지 않을 때는 “이 method가 정확히 무엇을 하는가?”, “두 가지 일을 섞고 있지 않은가?”, “abstraction boundary가 자연스러운가?”를 물어야 한다.

Name은 code review에서 반드시 다뤄야 한다. Author는 자신이 만든 name에 익숙해져 문제를 못 볼 수 있다. Reviewer가 name을 보고 behavior를 추측한 뒤 실제 code와 맞는지 확인하면 좋은 feedback을 줄 수 있다. Name이 reader의 첫 추측을 맞게 만드는지가 핵심 기준이다.

39. Chapter 15 — Write The Comments First

저자는 comment를 code 작성 후의 장식이 아니라 design tool로 본다. 그래서 write the comments first를 권한다. 특히 interface comment를 먼저 쓰면, code를 구현하기 전에 API가 어떤 abstraction을 제공하는지 명확히 할 수 있다.

Comment를 나중에 쓰면 나쁜 comment가 되기 쉽다. Code를 이미 작성한 뒤에는 implementation detail이 머릿속에 강하게 남아 있어 interface abstraction보다 how를 설명하게 된다. 또한 시간이 부족해져 comment를 생략하거나, code를 line by line으로 반복하는 comment를 쓰기 쉽다.

반대로 comment를 먼저 쓰면 caller 관점에서 생각하게 된다. “이 method는 무엇을 promise하는가?”, “parameter는 어떤 의미인가?”, “return value와 side effect는 무엇인가?”, “이 behavior를 설명하기 쉬운가?”를 구현 전에 점검할 수 있다. 만약 comment가 길고 복잡하다면 API design이 복잡하다는 신호다.

Comment-first approach는 TDD와 비슷한 면이 있다. Test를 먼저 쓰면 desired behavior를 명확히 하듯, comment를 먼저 쓰면 desired abstraction을 명확히 한다. Comment는 compile되지 않지만, design clarity를 검사한다. 좋은 interface comment는 implementation을 안내하는 spec 역할을 한다.

40. Chapter 15 — Early Comment는 비용이 아니라 절약이다

많은 developer는 comment를 먼저 쓰면 느려진다고 생각한다. 하지만 저자는 오히려 early comment가 design mistake를 줄여 시간을 절약한다고 본다. Code를 많이 작성한 뒤 API가 잘못되었음을 발견하는 것보다, comment 작성 중 interface가 설명하기 어렵다는 것을 발견하는 편이 훨씬 싸다.

Early comment는 재미있을 수도 있다. 아직 implementation detail에 갇히기 전에는 abstraction을 자유롭게 설계할 수 있다. Module이 외부에 어떤 모습으로 보이면 좋을지 상상하고, 가장 단순한 mental model을 만들 수 있다. 이 과정에서 module의 responsibility와 boundary가 더 명확해진다.

Implementation comment도 먼저 쓸 수 있다. 복잡한 algorithm을 작성하기 전에 block-level comment로 algorithm의 high-level step을 적으면, code structure가 자연스럽게 잡힌다. 이 방식은 특히 긴 method나 performance-critical code에서 유용하다. Comment가 outline 역할을 하고, code가 그 outline을 채운다.

물론 comment를 먼저 쓴 뒤 code가 바뀌면 comment도 업데이트해야 한다. 하지만 이것은 나쁜 점이 아니라 design과 documentation이 함께 진화한다는 뜻이다. Comment를 source code 가까이에 두고 review process에서 함께 확인하면 outdated 문제를 줄일 수 있다.

41. Chapter 16 — Modifying Existing Code

Existing code를 수정할 때도 strategic mindset이 필요하다. 많은 developer는 기존 design이 마음에 들지 않아도 “내 change만 최소로 하자”고 생각한다. 그러나 작은 patch가 bad design을 더 굳히면 장기 complexity가 증가한다. 저자는 기존 code를 수정할 때 주변 design을 조금씩 개선하라고 말한다.

물론 매번 큰 refactoring을 하라는 뜻은 아니다. 변경 범위를 무리하게 키우면 risk가 커진다. 핵심은 task와 관련된 부분에서 design을 더 나쁘게 만들지 말고, 가능하면 조금 더 낫게 만드는 것이다. 예를 들어 새 special case를 추가하기 전에 existing special case를 general mechanism으로 바꿀 수 있는지 살펴본다. 새 parameter를 pass-through하기 전에 context나 better abstraction이 필요한지 생각한다.

Comment maintenance도 중요한 주제다. Comment는 code 근처에 있어야 한다. Commit log나 external document에만 rationale을 두면 code를 읽는 사람이 찾기 어렵다. Commit log는 history를 설명하는 데 유용하지만, current behavior를 이해하기 위한 primary documentation이 되어서는 안 된다.

Comment duplication은 피해야 한다. 같은 정보를 여러 comment에 반복하면 하나만 update되어 inconsistency가 생긴다. Design decision을 한 곳에 설명하고, 필요하면 다른 곳에서는 짧게 참조하는 것이 좋다. Code duplication과 마찬가지로 documentation duplication도 change amplification을 만든다.

42. Chapter 16 — Diff를 통해 Comment를 유지하기

Comment를 최신 상태로 유지하는 practical technique 중 하나는 diff를 확인하는 것이다. Code change를 review할 때 nearby comment가 여전히 맞는지 함께 확인한다. 특히 method signature, parameter meaning, return behavior, exception policy, invariant가 바뀌면 interface comment를 반드시 update해야 한다.

Higher-level comment는 lower-level comment보다 유지하기 쉬운 경우가 많다. Implementation detail을 너무 많이 설명하는 comment는 code가 조금만 바뀌어도 outdated된다. 반대로 abstraction, purpose, invariant, rationale을 설명하는 comment는 더 stable하다. 따라서 comment는 가능한 한 stable한 concept을 설명하되, caller에게 필요한 precise contract는 놓치지 않아야 한다.

Existing code를 수정할 때 중요한 태도는 respect와 courage의 balance다. 기존 design에는 이유가 있을 수 있으므로 무시하면 안 된다. 동시에 나쁜 구조를 계속 방치하면 system은 점점 더 complex해진다. 변경을 하면서 design을 이해하고, 필요한 작은 개선을 하고, comment를 함께 업데이트하는 습관이 codebase를 건강하게 유지한다.

실전 질문은 다음과 같다. 이 change가 기존 abstraction을 강화하는가, 약화하는가? 새로운 special case나 dependency를 추가하는가? Comment와 naming은 새로운 behavior를 반영하는가? 이 patch를 본 future developer가 design intent를 이해할 수 있는가? 작은 refactoring으로 change를 더 clean하게 만들 수 있는가?

43. Chapter 17 — Consistency

Consistency는 complexity를 줄이는 강력한 도구다. System 안에서 비슷한 일이 비슷한 방식으로 표현되면, developer는 한 곳에서 배운 지식을 다른 곳에 적용할 수 있다. Naming, coding style, error handling, API shape, file organization, locking pattern, test structure, configuration format 모두 consistency의 대상이다.

Consistency가 있으면 cognitive load가 줄어든다. Reader는 매번 새 rule을 배울 필요가 없다. 예를 들어 모든 asynchronous operation이 같은 callback convention을 사용하고, 모든 error response가 같은 format을 사용하며, 모든 resource ownership이 같은 naming rule을 따른다면 code를 읽기 쉬워진다.

Consistency를 유지하려면 convention을 명시해야 한다. 팀 안에서 암묵적으로만 존재하는 rule은 쉽게 깨진다. Style guide, API guideline, design pattern example, code review checklist가 도움이 된다. 그러나 가장 중요한 것은 기존 code를 따르는 것이다. 새 code가 기존 pattern과 다르면 reader는 차이에 의미가 있다고 생각한다. 의미 없는 차이는 혼란을 만든다.

Consistency를 깨야 할 때도 있다. 기존 convention이 명백히 나쁘거나, 새로운 situation에 맞지 않거나, 더 좋은 abstraction이 필요한 경우다. 하지만 consistency를 깨는 decision은 의식적으로 해야 하며, 이유가 있어야 한다. 단순한 개인 취향 때문에 pattern을 바꾸면 system 전체의 obviousness가 떨어진다.

44. Chapter 17 — Consistency를 과하게 적용하지 않기

Consistency도 과하면 문제가 된다. 비슷하지 않은 것을 억지로 같은 모양으로 만들면 abstraction이 왜곡된다. 예를 들어 모든 class에 동일한 lifecycle method를 강제하거나, 모든 service가 같은 configuration 구조를 가져야 한다고 고집하면, 특정 service의 natural interface가 복잡해질 수 있다.

중요한 기준은 같은 concept에는 같은 표현을 쓰고, 다른 concept에는 다른 표현을 쓰는 것이다. 차이가 의미를 갖는다면 다르게 표현해야 한다. Consistency는 uniformity와 다르다. Uniformity는 외형을 같게 만드는 것이고, consistency는 의미 있는 pattern을 유지하는 것이다.

Consistency는 design review에서 특히 유용하다. Reviewer는 “이 code가 system의 다른 부분과 같은 방식으로 동작하는가?”, “다른 이름을 쓴 이유가 있는가?”, “기존 error handling convention과 맞는가?”, “새 abstraction이 기존 pattern과 어떻게 관계되는가?”를 물을 수 있다.

이 chapter는 책의 여러 주제와 연결된다. Good naming은 consistency 없이는 힘을 잃고, comment convention은 consistency가 있어야 유지되며, API design도 consistent해야 caller가 쉽게 배운다. Consistency는 작은 rule처럼 보이지만, large codebase에서 complexity를 낮추는 핵심 mechanism이다.

45. Chapter 18 — Code Should be Obvious

이 chapter의 핵심은 code가 reader에게 obvious해야 한다는 것이다. Obvious code는 빠르게 읽을 수 있고, reader의 첫 추측이 맞는 code다. Nonobvious code는 behavior나 meaning을 이해하기 위해 많은 추론이 필요하고, 잘못된 추측을 유도한다.

Obviousness는 writer가 아니라 reader 기준이다. Author는 자신이 작성한 code의 context를 알기 때문에 obvious하다고 느낄 수 있다. 하지만 reviewer나 new team member가 다르게 이해한다면 code는 nonobvious하다. 그래서 code review가 중요하다. Reviewer의 confusion은 design을 개선할 수 있는 귀중한 signal이다.

Code를 obvious하게 만드는 방법은 이미 여러 chapter에서 나왔다. Precise name, good comment, consistency, deep module, information hiding, simple interface가 모두 obviousness에 기여한다. 여기에 formatting과 whitespace도 중요하다. Blank line은 logical block을 나누고, indentation과 spacing은 expression structure를 보여준다. Parameter documentation도 whitespace를 통해 읽기 쉽게 구성할 수 있다.

Comment는 nonobvious code를 보완하는 데 사용된다. 하지만 comment가 필요하다고 해서 code를 그대로 두면 안 된다. 가능한 경우 code를 단순화하고, 그래도 남는 complexity를 comment로 설명해야 한다. 특히 performance optimization, event-driven callback, concurrency, error recovery처럼 본질적으로 nonobvious한 영역은 comment가 reader의 mental model을 도와야 한다.

46. Chapter 18 — Code를 덜 obvious하게 만드는 것들

event-driven programming은 유용하지만 control flow를 따라가기 어렵게 만든다. Handler는 직접 호출되지 않고 framework나 event loop에 의해 호출된다. Reader는 “이 function이 언제 호출되는가?”, “어떤 thread에서 실행되는가?”, “어떤 state가 준비되어 있는가?”를 알아야 한다. 따라서 handler interface comment에는 invocation condition과 context를 명시해야 한다.

Generic container나 overly generic abstraction도 nonobviousness를 만들 수 있다. Type이나 semantic meaning이 사라지면 reader는 object가 실제로 무엇을 담는지 알기 어렵다. Abstraction이 너무 general해지면 caller의 intent가 흐려진다. Chapter 6의 general-purpose module과 혼동하면 안 된다. 좋은 general-purpose abstraction은 더 본질적인 개념을 제공하지만, 지나치게 generic한 구조는 의미를 지운다.

Declaration과 initialization이 멀리 떨어져 있거나, variable이 너무 긴 scope를 가지거나, side effect가 hidden되어 있는 code도 nonobvious하다. Reader는 value가 어디서 바뀌는지 추적해야 한다. 가능하면 variable scope를 줄이고, state transition을 명확히 하고, side effect가 있는 operation은 name과 interface에서 드러나게 해야 한다.

Red flag는 Nonobvious Code다. Code의 behavior나 meaning이 quick reading으로 이해되지 않으면 멈춰서 개선해야 한다. 개선 방법은 naming, structure, comment, abstraction, API redesign 중 하나일 수 있다. 핵심은 “작동한다”가 아니라 “읽는 사람이 올바르게 이해한다”다.

이 chapter는 여러 software development trend를 책의 principle로 평가한다. 먼저 object-oriented programming은 information hiding, abstraction, interface/implementation separation을 지원할 수 있는 강력한 도구다. 그러나 OOP 자체가 좋은 design을 보장하지는 않는다. Class가 shallow하고 interface가 복잡하면 OOP를 써도 complexity는 증가한다.

Inheritance는 두 종류로 나눌 수 있다. interface inheritance는 여러 class가 같은 interface를 구현하는 것이다. 이는 complexity를 줄이는 데 도움이 될 수 있다. Caller는 하나의 interface를 배우고 여러 implementation을 사용할 수 있다. 예를 들어 I/O stream interface나 collection interface가 그렇다.

implementation inheritance는 parent class의 code와 state를 subclass가 재사용하는 것이다. 이것은 더 위험하다. Parent와 child 사이에 hidden dependency가 생기고, parent의 instance variable을 subclass가 공유하거나 override behavior에 의존하면 이해하기 어려운 coupling이 생긴다. Subclass를 이해하려면 parent implementation을 알아야 하고, parent를 수정하면 여러 subclass가 깨질 수 있다.

저자는 implementation inheritance를 신중하게 사용하라고 말한다. 가능하면 composition을 고려한다. 꼭 inheritance가 필요하다면 parent state와 child state를 분리하고, subclass가 parent internal detail에 의존하지 않게 해야 한다. Inheritance는 abstraction을 줄 수도 있지만, 잘못 쓰면 information leakage와 dependency explosion을 만든다.

48. Chapter 19 — Agile, Unit Tests, TDD

Agile development는 incremental and iterative development를 강조한다는 점에서 software design에 잘 맞는다. 작은 단위로 구현하고 feedback을 받아 design을 개선할 수 있기 때문이다. 그러나 Agile이 feature delivery에만 집중하면 tactical programming을 강화할 위험이 있다. “이번 sprint의 feature”를 끝내는 데만 몰두하고 abstraction을 개선하지 않으면 complexity가 쌓인다.

저자의 중요한 문장은 development increment가 feature가 아니라 abstraction이어야 한다는 것이다. Feature를 구현하더라도, 그 feature를 가능하게 하는 clean abstraction을 함께 만들어야 한다. 그렇게 하면 다음 feature가 더 쉬워진다. 반대로 feature-specific patch만 쌓이면 codebase는 점점 brittle해진다.

Unit test는 design에 긍정적이다. Test suite가 있으면 refactoring을 더 안전하게 할 수 있다. Strategic programming에는 design improvement가 필요하고, design improvement는 behavior preservation에 대한 confidence가 필요하다. Unit test는 system test보다 더 작은 범위를 커버하므로 bug를 빠르게 찾고, module interface를 검증하는 데 유용하다.

TDD는 test를 code보다 먼저 쓰는 practice다. 저자는 TDD의 장점을 인정하면서도, test-first가 design-first를 대체해서는 안 된다고 본다. Test를 먼저 쓰면 behavior를 명확히 할 수 있지만, 좋은 abstraction이 자동으로 나오지는 않는다. Test가 overly specific implementation detail에 묶이면 오히려 refactoring을 어렵게 만들 수 있다. Test는 design을 지원해야지 design을 대신할 수 없다.

49. Chapter 19 — Design Patterns, Getters and Setters

Design pattern은 반복되는 design problem에 대한 named solution을 제공한다. Pattern name은 communication을 쉽게 하고, 검증된 structure를 재사용하게 한다. 하지만 pattern을 기계적으로 적용하면 shallow class와 unnecessary layer가 생길 수 있다. Pattern은 목적이 아니라 도구다. 현재 problem에 실제로 complexity를 줄이는지 판단해야 한다.

예를 들어 decorator pattern은 특정 상황에서는 유용하지만 Chapter 7에서 본 것처럼 API duplication과 boilerplate를 만들 수 있다. Factory, strategy, observer 같은 pattern도 마찬가지다. 이들이 deep abstraction을 만들고 information hiding을 개선한다면 좋다. 하지만 단순한 problem에 pattern을 적용해 class만 늘리면 complexity가 증가한다.

Getter와 setter도 비판적으로 봐야 한다. Field를 private으로 만들고 getter/setter를 제공한다고 해서 information hiding이 자동으로 달성되지 않는다. Getter/setter가 internal representation을 그대로 노출하면 caller는 여전히 object 내부 state에 의존한다. 진짜 encapsulation은 caller가 원하는 higher-level operation을 제공하고, internal state manipulation을 object 내부에 숨기는 것이다.

이 chapter의 결론은 trend를 맹목적으로 따르지 말라는 것이다. OOP, Agile, unit test, TDD, design pattern, getter/setter 모두 유용할 수 있지만, 각각이 complexity에 어떤 영향을 주는지 평가해야 한다. 좋은 design principle은 tool보다 상위에 있다.

50. Chapter 20 — Designing for Performance

Performance와 clean design은 대립하지 않는다. 저자는 좋은 design이 high performance와 양립할 수 있다고 말한다. 실제로 simple code는 complex code보다 빠른 경우가 많다. Special case와 unnecessary layer가 줄면 branch, call overhead, cache miss, memory allocation도 줄어들 수 있다.

Performance를 생각할 때 첫 번째 원칙은 기본 cost model을 이해하는 것이다. 어떤 operation이 비싼지 알아야 design decision을 잘 할 수 있다. Network round trip, disk I/O, synchronization, cache miss, memory allocation, system call, serialization 같은 비용은 system performance에 큰 영향을 준다. Micro-benchmark는 특정 operation의 cost를 파악하는 데 도움이 된다.

하지만 intuition만으로 optimization하면 안 된다. Measure before and after modifying가 핵심이다. 최적화 전에 bottleneck을 측정하고, 변경 후 실제 improvement를 확인해야 한다. 많은 performance tuning은 잘못된 부분을 최적화하거나, complexity만 늘리고 meaningful gain을 만들지 못한다.

성능 문제를 해결할 때 가장 좋은 것은 fundamental fix다. 불필요한 data copy를 없애거나, algorithmic complexity를 줄이거나, batching으로 network round trip을 줄이거나, data layout을 cache-friendly하게 바꾸는 것이다. 이런 fix는 때때로 design을 더 단순하게 만들기도 한다. 단순한 local tweak보다 abstraction을 다시 설계하는 것이 더 큰 성능 개선을 가져올 수 있다.

51. Chapter 20 — Critical Path 중심 설계와 RAMCloud Buffer

Performance optimization에서는 critical path를 중심으로 design해야 한다. Common case에서 반드시 실행되는 최소 code path가 무엇인지 생각하고, 그 path에서 unnecessary layer, special case check, repeated computation, allocation, copy를 제거한다. Existing structure에 갇히지 말고, ideal fast path를 먼저 상상한 뒤 현재 design을 그 방향으로 refactor한다.

RAMCloud Buffer class 예시는 이 원칙을 보여준다. Buffer는 variable-length byte array를 discontiguous memory chunk로 관리해 data copy를 줄이는 abstraction이다. 원래 implementation은 기능적으로 동작했지만, common operation인 작은 internal allocation path에 여러 shallow method call과 conditional branch가 있었다. Refactoring 후 critical path를 단순화하면서 성능이 개선되었고 code도 더 읽기 쉬워졌다.

이 사례에서 중요한 점은 performance optimization이 design cleanup과 함께 이루어졌다는 것이다. 많은 사람은 성능을 위해 code가 더 복잡해져야 한다고 생각하지만, 여기서는 shallow abstraction을 제거하고 common path를 명확히 하면서 speed와 simplicity를 동시에 얻었다. 물론 모든 optimization이 이렇게 아름답지는 않다. 때로는 성능을 위해 complexity를 추가해야 한다. 그럴 때는 complexity를 잘 숨기고, interface를 오염시키지 않으며, comment로 rationale을 남겨야 한다.

Performance design의 checklist는 다음과 같다. Common case는 무엇인가? Critical path에 불필요한 abstraction layer가 있는가? Special case가 common path를 오염시키는가? Costly operation을 피할 수 있는 fundamental design change가 있는가? Measurement가 실제 bottleneck을 뒷받침하는가? Optimization 후 interface가 더 복잡해지지는 않았는가?

52. Chapter 21 — Decide What Matters

좋은 software design의 중요한 능력은 무엇이 중요한지 결정하는 것이다. 모든 것을 강조하면 아무것도 강조되지 않는다. 중요한 concept은 design의 중심에 두고, 중요하지 않은 detail은 숨기거나 default로 처리하거나 rare path로 밀어야 한다.

이 원칙은 abstraction과 직접 연결된다. Interface는 중요한 것을 드러내고, implementation은 덜 중요한 detail을 숨긴다. Naming은 중요한 property를 반영해야 하고, comment는 중요한 rationale을 설명해야 한다. Performance optimization도 중요한 path를 중심으로 해야 한다. Error handling도 caller에게 중요한 distinction만 노출해야 한다.

무엇이 중요한지 판단하는 방법 중 하나는 leverage를 찾는 것이다. 어떤 concept을 이해하면 많은 behavior를 설명할 수 있는가? 어떤 abstraction을 만들면 여러 feature가 쉬워지는가? 어떤 design decision을 바꾸면 많은 problem이 사라지는가? 이런 point가 important point다.

중요한 것을 최소화하는 것도 중요하다. Constructor parameter가 많고, caller가 많은 option을 알아야 하며, 여러 state를 조합해야 object를 사용할 수 있다면 너무 많은 것이 중요해진 것이다. 좋은 design은 caller가 신경 써야 하는 것의 수를 줄인다. Sensible default, deep module, information hiding은 모두 “what matters”를 줄이는 방법이다.

53. Chapter 21 — Emphasis와 Good Taste

중요한 것을 정했다면 design에서 강조해야 한다. 중요한 concept은 interface, name, documentation, module boundary에서 잘 보이는 위치에 있어야 한다. 반대로 중요하지 않은 detail은 hidden되거나 de-emphasized되어야 한다. Reader가 자주 보는 것이 곧 system에서 중요하다는 signal이 된다.

두 가지 mistake가 있다. 첫째, 너무 많은 것을 중요하게 취급하는 것이다. 모든 option, 모든 special case, 모든 internal detail을 interface에 올리면 design이 복잡해진다. 둘째, 정말 중요한 것을 알아보지 못하는 것이다. 중요한 invariant나 behavior가 숨겨져 있으면 developer는 잘못된 변경을 하게 된다.

저자는 “good taste”를 중요한 것과 중요하지 않은 것을 구분하는 능력으로 본다. Good taste는 타고나는 감각만이 아니라 experience와 reflection으로 키울 수 있다. 여러 design alternative를 비교하고, code review에서 confusion을 관찰하고, complexity가 어디서 생기는지 되돌아보면 taste가 좋아진다.

이 chapter는 책 전체를 압축한다. 좋은 design은 많은 것을 할 수 있는 code가 아니라, 중요한 것을 쉽게 이해하고 다룰 수 있게 하는 code다. Software designer의 일은 모든 detail을 드러내는 것이 아니라, 적절한 detail을 적절한 abstraction level에 배치하는 것이다.

54. Chapter 22 — Conclusion

책의 결론은 단순하지만 강하다. Software design의 가장 큰 적은 complexity이고, complexity는 작은 decision이 누적되어 생긴다. 따라서 좋은 design은 거대한 initial architecture보다 매일의 coding practice에서 만들어진다. Module을 deep하게 만들고, information hiding을 적용하고, unnecessary exception을 줄이고, naming과 comment를 정성스럽게 다루고, consistency를 유지하고, code를 obvious하게 만들 때 system은 오래 건강하게 유지된다.

저자는 완벽한 rulebook을 제공하지 않는다. 각 principle에는 tradeoff가 있고, “taking it too far”가 있다. General-purpose module이 좋지만 너무 general하면 overengineering이 된다. Complexity를 downward로 끌어내리는 것이 좋지만 모든 것을 한 class에 넣으면 안 된다. Consistency가 좋지만 의미 있는 차이까지 지워서는 안 된다. 따라서 책의 principle은 mechanical rule이 아니라 thinking tool이다.

이 책의 실전적 가치는 red flag를 언어화한 데 있다. Shallow Module, Information Leakage, Temporal Decomposition, Pass-Through Method, Vague Name, Nonobvious Code 같은 이름을 갖게 되면 code review에서 문제를 더 쉽게 공유할 수 있다. “느낌상 별로다”가 아니라 “이 class는 interface에 비해 benefit이 작아서 shallow하다”라고 말할 수 있다.

마지막으로, 좋은 design은 다른 사람에 대한 배려다. Future maintainer가 덜 고생하게 하고, caller가 덜 알아도 되게 하고, reviewer가 빠르게 이해하게 하며, system이 변화에 더 잘 견디게 한다. Complexity는 완전히 사라지지 않지만, 좋은 designer는 그것을 줄이고 숨기고 국소화한다.

55. Summary of Design Principles — 한국어 정리

다음은 책 뒤쪽의 design principle 요약을 한국어로 재구성한 것이다.

  1. Complexity is incremental. 작은 design compromise가 누적되어 큰 complexity가 된다. 작은 것에 신경 써야 한다.
  2. Working code isn’t enough. Code가 동작하는 것만으로 task가 끝난 것이 아니다. Maintainability와 design quality도 결과물이다.
  3. Make continual small investments. System design을 개선하기 위한 작은 investment를 계속하라.
  4. Modules should be deep. 좋은 module은 simple interface 뒤에 많은 functionality와 complexity를 숨긴다.
  5. Common usage should be simple. Interface는 가장 흔한 사용을 가장 쉽게 만들어야 한다.
  6. Simple interface matters more than simple implementation. Module author가 조금 더 복잡한 implementation을 감수하더라도 caller interface를 단순하게 만드는 것이 중요하다.
  7. General-purpose modules are deeper. 너무 specific한 API보다 본질적인 operation을 제공하는 somewhat general-purpose API가 더 깊다.
  8. Separate general-purpose and special-purpose code. Mechanism과 policy, reusable logic과 use-case-specific logic을 섞지 말라.
  9. Different layers should have different abstractions. Layer가 추가되면 새로운 abstraction을 제공해야 한다.
  10. Pull complexity downward. 많은 caller가 반복해서 처리할 complexity는 module 내부로 끌어내려라.
  11. Define errors out of existence. API semantics를 바꿔 unnecessary error case를 없앨 수 있는지 생각하라.
  12. Design it twice. 중요한 design은 최소 두 가지 alternative를 비교하라.
  13. Comments should describe things not obvious from code. Code를 반복하지 말고, code만으로 알 수 없는 abstraction, rationale, constraint를 설명하라.
  14. Design for ease of reading, not ease of writing. Code는 쓰는 시간보다 읽히는 시간이 훨씬 길다.
  15. Development increments should be abstractions, not features. Feature를 붙이는 데 그치지 말고 다음 feature를 쉽게 만드는 abstraction을 만들어라.
  16. Separate what matters from what doesn’t. 중요한 것은 강조하고, 덜 중요한 것은 숨기거나 default로 처리하라.

56. Summary of Red Flags — 한국어 정리

다음 red flag들은 design 문제가 있을 가능성을 알려주는 신호다.

  • Shallow Module: Interface를 배우는 비용에 비해 제공하는 functionality가 작다.
  • Information Leakage: 하나의 design decision이 여러 module에 반영되어 있다.
  • Temporal Decomposition: Code 구조가 information hiding이 아니라 실행 순서를 기준으로 나뉘어 있다.
  • Overexposure: Common case caller가 rare feature나 internal detail을 알아야 한다.
  • Pass-Through Method: 거의 같은 signature로 다른 method에 argument를 넘기기만 한다.
  • Repetition: Nontrivial logic이나 decision이 여러 곳에서 반복된다.
  • Special-General Mixture: General-purpose code와 special-purpose code가 섞여 있다.
  • Conjoined Methods: 두 method가 너무 강하게 의존해서 따로 이해하기 어렵다.
  • Comment Repeats Code: Comment가 주변 code에서 obvious한 정보만 반복한다.
  • Implementation Documentation Contaminates Interface: Interface comment가 caller에게 필요 없는 implementation detail을 설명한다.
  • Vague Name: Name이 precise한 정보를 전달하지 못한다.
  • Hard to Pick Name: 좋은 name을 찾기 어렵다면 entity의 responsibility가 불명확할 수 있다.
  • Hard to Describe: Complete documentation이 지나치게 길어져야 한다면 abstraction이 나쁠 수 있다.
  • Nonobvious Code: Quick reading으로 behavior나 meaning을 이해하기 어렵다.

Red flag는 자동 판정 규칙이 아니다. 발견되면 “왜 이런 구조가 되었는가?”, “다른 abstraction이 가능한가?”, “이 complexity를 없애거나 숨길 수 있는가?”를 묻는 계기로 사용한다.

57. Code Review Checklist — Module과 Interface

Design review나 code review에서 바로 사용할 수 있는 질문들이다.

Module depth

  • 이 module은 interface complexity보다 훨씬 많은 value를 제공하는가?
  • Caller가 이 module을 쓰기 위해 알아야 하는 information은 최소인가?
  • Common case가 simple path로 표현되는가?
  • Rare case나 special option이 common case interface를 오염시키는가?
  • 이 module을 제거하고 caller가 직접 구현하면 얼마나 많은 complexity가 퍼지는가?

Interface contract

  • Formal interface뿐 아니라 informal interface도 명확한가?
  • Hidden precondition, call ordering, side effect, ownership rule이 있는가?
  • Exception은 interface를 불필요하게 넓히고 있지 않은가?
  • Caller가 implementation detail을 알아야 하는가?
  • Interface comment만 읽고 올바르게 사용할 수 있는가?

Information hiding

  • 중요한 design decision이 한 곳에 localize되어 있는가?
  • Data representation을 바꾸면 몇 module이 바뀌는가?
  • 같은 assumption이 여러 곳에 반복되어 있지 않은가?
  • Module boundary가 knowledge boundary로 작동하는가?
  • Cross-module invariant는 discoverable한가?

58. Code Review Checklist — Error, Naming, Comment

Error handling

  • 이 error case를 API definition으로 없앨 수 있는가?
  • Caller가 exception을 meaningful하게 처리할 수 있는가?
  • Lower layer가 retry, normalization, default, cleanup으로 mask할 수 있는가?
  • 여러 handler를 aggregate할 수 있는가?
  • Error path가 test 가능하고 readable한가?

Naming

  • Name을 보고 reader의 첫 추측이 맞을까?
  • 같은 concept에 같은 term을 쓰고 있는가?
  • 다른 concept이 같은 name family를 공유해 혼란을 만들지 않는가?
  • Unit, range, state, ownership이 중요한 경우 name에 드러나는가?
  • Name을 고르기 어렵다면 responsibility가 섞인 것은 아닌가?

Comment

  • Comment가 code를 반복하지 않는가?
  • Interface comment가 implementation detail로 오염되지 않았는가?
  • Implementation comment가 what/why를 설명하는가?
  • Complex invariant, concurrency rule, performance rationale이 설명되어 있는가?
  • Code change와 함께 nearby comment가 update되었는가?

59. Code Review Checklist — Structure와 Performance

Structure

  • 이 class/method를 나눈 이유가 line 수 때문인가, abstraction 때문인가?
  • Split된 method들이 독립적으로 이해되는가?
  • Conjoined Methods가 생기지 않았는가?
  • Pass-through method나 pass-through variable이 많은가?
  • Decorator/wrapper가 실제 benefit보다 boilerplate를 더 많이 만들지 않는가?

Consistency and obviousness

  • 기존 codebase의 convention과 일치하는가?
  • 차이가 있다면 의미 있는 차이인가?
  • Formatting과 whitespace가 logical structure를 보여주는가?
  • Event-driven callback의 invocation context가 명확한가?
  • Reader가 quick reading으로 behavior를 이해할 수 있는가?

Performance

  • 실제 bottleneck을 measurement로 확인했는가?
  • Critical path가 명확한가?
  • Common path에 unnecessary layer나 special case가 있는가?
  • Optimization이 interface를 복잡하게 만들지는 않았는가?
  • Performance rationale이 comment로 남아 있는가?

60. Systems/OS 관점 적용 메모

OS, kernel, distributed system, storage system 같은 영역에서는 이 책의 원칙이 특히 강하게 적용된다. 이런 system은 concurrency, failure, performance, resource ownership, hardware interaction 때문에 essential complexity가 크다. 따라서 accidental complexity를 줄이는 design이 더 중요하다.

Kernel API와 system call

System call은 deep module의 전형적인 예가 될 수 있다. User program은 read, write, mmap, fork, exec, ioctl 같은 interface를 통해 kernel의 complex implementation을 사용한다. 좋은 system call은 caller가 internal scheduling, device driver, page cache detail을 몰라도 되게 한다. 반대로 ioctl처럼 너무 generic하고 device-specific detail을 많이 노출하는 interface는 overexposure와 obscurity를 만들 수 있다.

Concurrency와 information hiding

Locking policy, memory ordering, ownership rule은 반드시 localize되어야 한다. Lock order가 여러 module에 암묵적으로 퍼져 있으면 deadlock과 race가 생긴다. 좋은 design은 synchronization을 lower-level abstraction에 숨기거나, 최소한 interface comment로 thread-safety contract를 명확히 한다.

Distributed system error

Distributed system은 exception의 유혹이 크다. Timeout, retry, duplicate request, partial failure를 모두 caller에게 노출하면 application code가 폭발한다. 가능한 경우 lower layer가 idempotency, retry, deduplication, session recovery를 제공해 error를 mask하거나 aggregate해야 한다. 그러나 false recovery가 data consistency를 해칠 수 있으므로, 어떤 failure가 caller에게 정말 중요한지 carefully decide해야 한다.

61. 이 책의 철학을 한 문장씩 다시 압축하기

  • Software design의 목적은 아름다운 diagram이 아니라 future change의 비용을 낮추는 것이다.
  • Complexity는 size가 아니라 이해와 수정의 difficulty다.
  • Complexity는 한 번에 폭발하기보다 작은 shortcut이 누적되어 자란다.
  • Good module은 caller에게 적은 것을 요구하고 많은 것을 제공한다.
  • Interface는 implementation보다 중요하다. 많은 사람이 interface를 보고 작업하기 때문이다.
  • Abstraction은 detail을 숨기는 것이 아니라 중요한 detail과 덜 중요한 detail을 구분하는 것이다.
  • Error handling은 interface design이다. Exception을 던지는 순간 caller의 complexity가 증가한다.
  • Comment는 code의 반복이 아니라 code가 말하지 못하는 design knowledge다.
  • Naming은 가장 짧은 documentation이다.
  • Consistency는 reader의 추측을 맞게 만드는 장치다.
  • Performance와 simplicity는 적이 아니다. Critical path를 잘 설계하면 둘을 함께 얻을 수 있다.
  • 좋은 designer는 무엇이 중요한지 결정하고, 중요한 것을 보이게 하며, 중요하지 않은 것을 숨긴다.

62. 실전 적용 순서: 기존 codebase를 개선할 때

이 책의 원칙을 codebase에 적용할 때 처음부터 대규모 rewrite를 시도할 필요는 없다. 다음 순서가 현실적이다.

  1. Red flag를 관찰한다. Shallow Module, Information Leakage, Pass-Through Method, Vague Name, Nonobvious Code를 찾는다.
  2. Common path부터 본다. 자주 수정되거나 자주 호출되는 부분의 complexity가 가장 큰 비용을 만든다.
  3. Interface를 먼저 단순화한다. Implementation cleanup보다 caller가 알아야 하는 정보를 줄이는 것이 우선이다.
  4. Design decision을 localize한다. 같은 assumption이 여러 곳에 있으면 한 곳으로 모은다.
  5. Exception을 줄인다. Error를 normal semantics로 흡수하거나, lower layer에서 mask하거나, top-level에서 aggregate한다.
  6. Name과 comment를 고친다. 작은 변경이지만 reader의 cognitive load를 크게 줄인다.
  7. Test로 refactoring을 보호한다. Unit test와 integration test가 있으면 strategic programming을 더 안전하게 할 수 있다.
  8. 매 change마다 조금 개선한다. Design quality는 한 번의 heroic rewrite보다 지속적인 discipline으로 유지된다.

이 순서는 absolute rule이 아니다. 그러나 중요한 점은 “design improvement”를 별도 project로만 미루지 않는 것이다. Feature work와 maintenance work 속에서 작은 구조 개선을 계속해야 complexity 증가를 늦출 수 있다.

63. 마지막 요약: 읽고 나서 남겨야 할 습관

이 책을 읽고 가장 크게 바뀌어야 할 습관은 code를 작성할 때 계속 “누가 무엇을 알아야 하는가?”를 묻는 것이다. Caller가 알아야 하는 정보가 많아질수록 interface는 얕아진다. 여러 module이 같은 decision을 알아야 할수록 information leakage가 생긴다. Reader가 hidden assumption을 찾아야 할수록 obscurity가 커진다.

두 번째 습관은 “작동하는가?” 다음에 “obvious한가?”를 묻는 것이다. Code가 test를 통과해도, name이 모호하고 comment가 없고 special case가 흩어져 있으면 future change는 어려워진다. Good code는 현재 bug가 없는 code가 아니라, 미래의 bug를 덜 만들 code다.

세 번째 습관은 alternative를 생각하는 것이다. 첫 solution에 만족하지 말고 최소 한 번은 다른 module boundary, 다른 API, 다른 error semantics, 다른 data representation을 상상한다. 이 과정은 design taste를 키운다.

마지막 습관은 작은 design investment를 꾸준히 하는 것이다. Complexity는 매일 조금씩 증가한다. 따라서 simplicity도 매일 조금씩 회복해야 한다. Name 하나를 precise하게 바꾸고, exception 하나를 없애고, duplicate logic 하나를 합치고, interface comment 하나를 명확히 하는 일들이 결국 codebase의 장기 생존성을 결정한다.


Appendix A. 핵심 용어 Glossary

  • Complexity: 이해와 수정 비용을 증가시키는 software structure상의 어려움.
  • Change amplification: 작은 conceptual change가 많은 code change로 증폭되는 현상.
  • Cognitive load: 작업을 위해 developer가 머릿속에 유지해야 하는 정보량.
  • Unknown unknowns: 무엇을 모르는지조차 모르는 상태에서 발생하는 위험.
  • Dependency: 한 부분을 이해/수정하기 위해 다른 부분을 알아야 하는 관계.
  • Obscurity: 중요한 정보가 명확히 드러나지 않는 상태.
  • Module: Interface와 implementation을 가진 code unit.
  • Interface: Module을 사용하기 위해 외부 developer가 알아야 하는 모든 것.
  • Implementation: Interface의 promise를 실제로 수행하는 내부 code.
  • Abstraction: 중요한 정보를 보존하고 덜 중요한 detail을 숨긴 단순화된 model.
  • Information hiding: Design decision을 module 내부에 숨겨 external dependency를 줄이는 기법.
  • Information leakage: Design decision이 여러 module에 퍼져 있는 상태.
  • Deep module: Simple interface로 많은 functionality를 제공하는 module.
  • Shallow module: Interface 비용에 비해 제공하는 benefit이 작은 module.
  • Pass-through method: 별도 기능 없이 argument를 다른 method로 넘기는 method.
  • Pass-through variable: 중간 method들이 사용하지 않는데 call chain을 따라 전달되는 variable.
  • Define errors out of existence: API semantics를 바꿔 error case 자체를 없애는 방식.
  • Exception masking: Lower layer가 exception을 처리해 upper layer가 알 필요 없게 만드는 방식.
  • Exception aggregation: 여러 error를 한 곳에서 공통 처리하는 방식.
  • Critical path: Performance에 가장 중요한 common execution path.