코드 작성법: Complexity를 낮추고 Change를 쉽게 만드는 실전 가이드
목적: A Philosophy of Software Design, Clean Code, 그리고 비슷한 software engineering 책/글의 아이디어를 비교해서, 실제로 code를 작성하고 review하고 refactor할 때 쓸 수 있는 한국어 study guide로 정리한다.
용어 원칙: complexity, abstraction, module, interface, refactoring, test, code review, ownership, invariant 같은 전문 용어는 English 그대로 둔다.
주의: 이 문서는 특정 책의 번역본이 아니라, 여러 자료를 바탕으로 재구성한 개인 학습용 synthesis이다. 원문 표현, 예시, case study는 각 책과 원문 자료를 참고해야 한다.
0. 한 문장 요약
좋은 code는 “짧고 예쁜 code”가 아니라, 다음 change를 안전하고 빠르게 만들도록 complexity를 적절한 곳에 숨기고, reader가 덜 추측하게 만드는 code다.
이 문서의 중심 thesis는 다음과 같다.
- 좋은 code의 제1 목적은
changeability다. readability는 중요하지만, 그 자체가 끝이 아니다. Readability는 change를 쉽게 하기 위한 조건이다.Clean Code의 micro-level discipline과A Philosophy of Software Design의 macro-level design 철학은 함께 쓰면 강력하지만, 그대로 합치면 충돌하는 지점이 있다.small function,DRY,comment minimization,pattern,TDD,SOLID같은 rule은 절대 규칙이 아니라 trade-off를 다루기 위한 heuristic이다.- Systems code에서는
resource ownership,lifetime,concurrency,error path,state transition,performance invariant가 readability만큼 중요하다.
1. 왜 “좋은 code”가 어려운가
Code 작성은 단순히 machine에게 instruction을 주는 일이 아니다. Machine은 모호한 이름, 복잡한 dependency, 흐릿한 abstraction에도 불평하지 않는다. 문제는 사람이다. 미래의 reader는 지금의 author가 아니다. 몇 달 뒤의 나도 지금의 나와 다르다. 좋은 code란 결국 미래의 reader가 덜 기억하고, 덜 추측하고, 덜 두려워하게 만드는 code다.
Software가 어려운 이유는 기능이 많아서만이 아니다. 기능의 수보다 더 큰 문제는 관계의 수다. 한 function의 behavior가 configuration, global state, lock ordering, hidden cache, build flag, retry policy, callback side effect, test fixture에 의존하기 시작하면 code의 실제 의미는 source file 안에 있지 않고 system 전체에 흩어진다. 이때 developer는 code를 읽는 것이 아니라 archaeological excavation을 하게 된다.
A Philosophy of Software Design은 이 문제를 complexity 중심으로 본다. Complexity는 system을 이해하고 수정하기 어렵게 만드는 모든 것이다. Clean Code는 더 가까운 거리에서 같은 문제를 본다. 나쁜 naming, 긴 function, 섞인 responsibility, 지저분한 formatting, 불충분한 test가 codebase를 읽기 어렵게 만들고, 결국 변경 비용을 키운다고 본다.
둘은 서로 다른 scale을 본다. Clean Code는 현미경에 가깝다. 한 function, 한 class, 한 name, 한 test를 어떻게 더 읽기 쉽게 만들지 묻는다. A Philosophy of Software Design은 지도에 가깝다. Module boundary, information hiding, abstraction depth, interface design, design decision의 위치를 묻는다.
좋은 engineer는 둘 다 필요하다. 현미경만 있으면 local cleanliness를 위해 system structure를 망칠 수 있다. 지도만 있으면 큰 구조는 그럴듯하지만 실제 function과 error path가 읽기 어려울 수 있다.
2. A Philosophy of Software Design와 Clean Code의 핵심 차이
2.1 비교표
| 관점 | A Philosophy of Software Design | Clean Code |
|---|---|---|
| 중심 질문 | 이 design은 complexity를 줄이는가? | 이 code는 읽기 쉽고 professional한가? |
| 주요 단위 | module, interface, abstraction, design decision |
function, class, name, test, smell |
| 좋은 구조 | deep module: simple interface 뒤에 많은 capability를 숨김 |
작고 명확한 function/class가 협력함 |
| 나쁜 구조 | shallow module, information leakage, pass-through method, temporal decomposition |
long function, bad name, duplicate code, mixed responsibility, missing tests |
| Comment 관점 | 좋은 comment는 interface, invariant, design rationale, nonobvious decision을 드러냄 | 좋은 code는 많은 comment가 필요 없도록 작성되어야 함. Comment는 failure를 보상하는 경우가 많음 |
| Function 길이 | 길이 자체보다 coherent abstraction이 중요함 | 가능한 작고 한 가지 일을 하는 function을 선호함 |
| Design 변화 | strategic programming: 오늘 조금 더 투자해 future complexity를 줄임 |
Boy Scout Rule: code를 만질 때 조금 더 깨끗하게 남김 |
| Test 역할 | design feedback과 safe change의 수단 | TDD와 unit test를 professional discipline의 핵심으로 강조 |
| 위험 | 너무 추상적인 말로 남으면 local coding habit을 못 고침 | rule을 문자 그대로 따르면 fragmentation과 shallow abstraction이 생길 수 있음 |
| 가장 잘 쓰이는 상황 | system/module/API 설계, library boundary, architecture review | daily coding, code review, refactoring, naming/formatting/test discipline |
2.2 충돌 1: “Function은 무조건 작아야 하는가?”
Clean Code는 작은 function을 강하게 선호한다. 작고 이름이 좋은 function은 reader가 한 번에 이해하기 쉽고, test하기 쉽고, 재사용하기 쉽다. 문제는 “작다”가 목적이 되면 function이 지나치게 잘게 쪼개져서 control flow가 흩어진다는 점이다.
A Philosophy of Software Design 관점에서는 어떤 split이든 interface를 만든다. Function을 분리하면 이름, parameter, return value, precondition, side effect라는 작은 interface가 생긴다. 이 interface가 실제로 complexity를 숨기면 좋은 split이다. 하지만 단지 세 줄을 한 줄처럼 감싸는 pass-through method라면 reader는 더 많은 symbol을 따라가야 한다. 이 경우 function 수는 늘었지만 abstraction depth는 늘지 않는다.
따라서 좋은 기준은 “작은가?”가 아니라 다음 질문이다.
- 이 function은 caller가 몰라도 되는 detail을 숨기는가?
- 이 이름은 내부 구현을 읽지 않아도 useful한 mental model을 주는가?
- 이 split이 change point를 줄이는가, 아니면 jump point만 늘리는가?
- 이 function은 coherent한 하나의 story를 말하는가?
나쁜 split 예시
static bool is_valid(struct request *req) {
return req != NULL && req->len > 0 && req->buf != NULL;
}
static bool should_process(struct request *req) {
return is_valid(req);
}
int handle_request(struct request *req) {
if (!should_process(req)) return -EINVAL;
return process_request(req);
}
여기서 should_process는 새로운 abstraction을 제공하지 않는다. 이름은 다르지만 사실상 is_valid를 통과시키는 wrapper다. 이런 split은 call graph만 늘린다.
좋은 split 예시
static bool request_has_well_formed_payload(const struct request *req) {
if (req == NULL || req->buf == NULL) return false;
if (req->len < HEADER_SIZE) return false;
if (req->len > MAX_REQUEST_SIZE) return false;
return checksum_ok(req->buf, req->len);
}
int handle_request(struct request *req) {
if (!request_has_well_formed_payload(req)) return -EINVAL;
return process_request(req);
}
여기서는 validation detail을 숨긴다. Caller는 payload가 well-formed인지 알고 싶을 뿐, header size, max size, checksum rule을 모두 알고 싶지 않다. 이 split은 interface를 통해 complexity를 줄인다.
2.3 충돌 2: “Comment는 나쁜 code의 냄새인가?”
Clean Code는 comment에 조심스럽다. Code가 명확하면 많은 comment가 필요하지 않으며, 낡은 comment는 거짓말이 되기 쉽다. 이 지적은 매우 중요하다. 다음 comment는 아무 가치가 없다.
# increment i by one
i += 1
하지만 A Philosophy of Software Design은 comment를 더 적극적으로 본다. 좋은 comment는 code만 봐서는 알 수 없는 정보를 담는다. 특히 interface comment, invariant, why, design rationale, concurrency rule, performance assumption, ownership contract는 code로 완전히 표현하기 어렵다.
Systems code에서는 좋은 comment가 더 중요해진다. 예를 들어 lock ordering, memory ordering, interrupt context, ownership transfer, syscall ABI compatibility는 code syntax만으로 충분히 obvious하지 않다.
/*
* Lock ordering: inode->lock must be acquired before page->lock.
* Reversing this order can deadlock with writeback.
*/
void attach_page_to_inode(struct inode *inode, struct page *page) {
...
}
이 comment는 code를 반복하지 않는다. Future maintainer가 깨뜨리면 안 되는 constraint를 보존한다. 이런 comment는 clean code의 적이 아니라 design의 일부다.
2.4 충돌 3: DRY와 duplication
Clean Code는 duplication을 강하게 경계한다. Duplication은 change amplification의 대표 원인이다. 하지만 premature deduplication도 위험하다. 표면적으로 비슷한 code를 너무 빨리 합치면 서로 다른 policy가 하나의 abstraction에 묶인다. 나중에 두 use case가 갈라질 때 abstraction이 더 복잡해진다.
좋은 기준은 “text가 같은가?”가 아니라 “같은 design decision인가?”다.
- 같은 requirement에서 나온 중복이면 합쳐라.
- 우연히 모양만 같은 code라면 기다려라.
- 두 caller가 같은 policy를 공유해야 한다면 abstraction으로 묶어라.
- 두 caller가 독립적으로 변할 수 있다면 duplication을 잠시 허용하라.
즉, DRY는 “모든 비슷한 text를 제거하라”가 아니라 “하나의 knowledge를 여러 곳에 흩뿌리지 말라”로 이해하는 편이 안전하다.
2.5 충돌 4: SOLID와 deep module
Clean Code 계열은 SOLID와 OO design을 자주 강조한다. Single Responsibility Principle, Open/Closed Principle, Dependency Inversion 등은 dependency를 관리하는 데 유용하다. 그러나 모든 것을 interface와 small class로 나누면 shallow module이 많아질 수 있다.
A Philosophy of Software Design 관점에서는 class 수보다 module depth가 중요하다. 깊은 module은 simple interface로 많은 일을 한다. 얕은 module은 interface는 많지만 실제 기능은 거의 없다. 지나친 interface 분리는 “유연성”처럼 보이지만, 실제로는 reader가 이해해야 할 indirection과 dependency를 늘릴 수 있다.
좋은 OO design은 class를 많이 만드는 것이 아니라, important design decision을 적절한 boundary 안에 숨기는 것이다.
3. 두 책을 함께 쓰는 방법
두 책의 조언을 통합하면 다음과 같은 원칙이 나온다.
3.1 Clean Code는 local hygiene로 사용하라
다음 문제에는 Clean Code식 discipline이 즉시 효과적이다.
- 이름이 모호하다.
- function이 여러 unrelated task를 한다.
- formatting이 뒤섞여 있다.
- error handling이 main logic을 가린다.
- test 없이 refactor하고 있다.
- class가 data bag인지 behavior owner인지 불분명하다.
- 한 파일 안에서 style이 일관되지 않다.
이런 문제는 code review에서 바로 잡아야 한다. Local hygiene가 무너지면 큰 design discussion도 힘들어진다. 지저분한 방에서 architecture를 논의하기 어렵다.
3.2 A Philosophy of Software Design은 boundary decision에 사용하라
다음 문제에는 A Philosophy of Software Design식 질문이 더 효과적이다.
- 이 module이 너무 많은 configuration을 caller에게 요구한다.
- 같은 design decision이 여러 component에 새어 나간다.
- API는 간단해 보이지만 사용하려면 hidden knowledge가 필요하다.
- shallow wrapper가 많아져 call graph가 길다.
- feature가 추가될 때마다 여러 subsystem을 동시에 수정한다.
- temporal order가 abstraction을 대신한다.
- special case가 점점 system 전체로 퍼진다.
이 문제들은 naming만 바꿔서는 해결되지 않는다. Boundary를 다시 정하고, policy를 한곳으로 모으고, caller가 알아야 할 정보를 줄여야 한다.
3.3 실제 판단 순서
Code를 작성하거나 review할 때 다음 순서로 보면 좋다.
- 이 change의 conceptual purpose는 무엇인가?
- 이 purpose가 code 구조에서 한 곳에 모여 있는가?
- Caller가 알아야 하는 정보가 너무 많지 않은가?
- Function/class/file 이름이 reader에게 올바른 expectation을 주는가?
- Error path, resource lifetime, concurrency invariant가 명확한가?
- Test가 behavior를 보호하는가, implementation detail에 과도하게 묶여 있는가?
- 나중에 가장 가능성 높은 change가 왔을 때 수정 지점은 어디인가?
이 순서는 local style보다 design decision을 먼저 보게 해준다. Style은 중요하지만, style이 좋은 나쁜 abstraction은 여전히 나쁜 abstraction이다.
4. 비슷한 책과 글들의 지도
4.1 Code Complete
Code Complete는 software construction의 encyclopedic guide에 가깝다. Naming, routine design, defensive programming, debugging, refactoring, construction planning 등 code 작성의 넓은 영역을 다룬다. A Philosophy of Software Design이 작은 책으로 design philosophy를 압축한다면, Code Complete는 construction practice를 넓게 펼친다.
이 책에서 가져올 태도는 “code quality는 우연히 생기지 않는다”는 점이다. 좋은 code는 design, coding, debugging, test, review, maintenance의 모든 단계에서 만들어진다. 특히 systems code를 쓰는 사람에게는 defensive programming, error handling, assertion, construction-time quality control이 유용하다.
4.2 Refactoring
Martin Fowler의 Refactoring은 기존 code를 behavior-preserving transformation으로 조금씩 개선하는 기술이다. 핵심은 “크게 갈아엎지 말고 작게 안전하게 움직이라”는 것이다. Refactoring은 design을 한 번에 완성하지 않고, working code에서 design을 회복하는 과정이다.
A Philosophy of Software Design이 “어떤 design이 좋은가?”를 묻는다면, Refactoring은 “현재 나쁜 design을 어떻게 안전하게 좋은 방향으로 움직일 것인가?”를 묻는다. 좋은 developer에게 둘은 세트다. 이상적인 design을 아는 것만으로는 충분하지 않다. 현실의 codebase는 이미 복잡하고, test가 부족하고, schedule pressure가 있다. 작은 transformation을 안전하게 쌓는 능력이 필요하다.
4.3 Working Effectively with Legacy Code
Michael Feathers의 Working Effectively with Legacy Code는 큰 untested codebase를 다루는 현실적인 책이다. Legacy code의 핵심 문제는 “나쁘다”가 아니라 “바꾸기 무섭다”다. Test가 없으면 change의 영향 범위를 알 수 없고, refactoring도 위험해진다.
이 책의 중요한 개념은 seam이다. Seam은 behavior를 바꾸지 않고 dependency를 끊거나 test double을 넣을 수 있는 지점이다. Legacy code에서는 먼저 seam을 찾고, characterization test로 현재 behavior를 붙잡고, 그다음 작은 refactoring을 수행한다.
4.4 The Pragmatic Programmer
The Pragmatic Programmer는 특정 code smell catalog라기보다 programmer로 일하는 태도를 다룬다. 책임감, 지속적 학습, duplication 회피, tool mastery, prototype, automation, testing, concurrency, requirements 이해 등 넓은 주제를 다룬다.
이 책의 강점은 code quality를 개인 habit과 team practice로 연결한다는 점이다. 좋은 code는 단지 syntax choice가 아니라, feedback loop를 짧게 만들고, 반복 작업을 automation하고, 모르는 것을 빨리 드러내는 방식에서 나온다.
4.5 Design Patterns
Design Patterns는 OO design에서 반복되는 solution vocabulary를 제공한다. Pattern은 이름 붙은 solution이므로 communication cost를 줄인다. Adapter, Strategy, Observer, Factory, Composite 같은 이름은 팀이 design을 빠르게 논의하게 해준다.
하지만 pattern은 badge가 아니다. Pattern을 쓰면 design이 좋아지는 것이 아니라, recurring problem과 trade-off가 실제로 있을 때 pattern vocabulary가 도움이 된다. Pattern을 억지로 적용하면 indirection이 늘고 shallow abstraction이 생긴다.
4.6 Parnas의 information hiding
David Parnas의 modular decomposition 논문은 module을 나누는 기준이 단순한 execution step이 아니라 숨겨야 할 design decision이어야 한다는 관점을 제시한다. 이 아이디어는 A Philosophy of Software Design의 information hiding과 매우 잘 맞는다.
좋은 module은 “이 일을 하는 step”이 아니라 “이 decision을 숨기는 boundary”다. 예를 들어 file format, scheduling policy, buffer layout, caching strategy, wire protocol, memory allocation policy는 module 내부로 숨기기 좋은 design decision이다.
4.7 Brooks의 No Silver Bullet와 Out of the Tar Pit
Brooks의 No Silver Bullet은 software의 어려움을 essential complexity와 accidental complexity로 나누어 생각하게 만든다. 모든 complexity를 없앨 수는 없다. 문제 domain 자체가 어려우면 code도 어느 정도 어려워질 수밖에 없다. 하지만 tool, language, architecture, dependency, state management 때문에 생기는 accidental complexity는 줄일 수 있다.
Out of the Tar Pit은 complexity를 software problem의 root cause로 강하게 본다. 특히 state와 control이 이해를 어렵게 만든다고 보고, accidental complexity를 피하는 방향을 강조한다. 이 글을 문자 그대로 특정 paradigm의 주장으로만 읽기보다, “state와 control flow가 reasoning cost를 만든다”는 경고로 읽는 편이 실용적이다.
4.8 Rich Hickey의 Simple Made Easy
Rich Hickey의 Simple Made Easy는 simple과 easy를 구분한다. Easy는 익숙하거나 당장 접근하기 쉬운 것이다. Simple은 얽혀 있지 않은 것이다. 익숙한 framework, 빠른 shortcut, 편한 global state는 easy할 수 있지만 simple하지 않을 수 있다.
Code 작성에서 이 구분은 매우 중요하다. 당장 쉬운 선택이 future complexity를 만들 수 있다. 반대로 처음엔 낯선 immutable data, explicit dependency, type-level state machine, structured concurrency는 easy하지 않을 수 있지만 long-term reasoning을 단순하게 만들 수 있다.
4.9 Google Engineering Practices
Google의 code review guide는 perfection보다 code health의 지속적 개선을 강조한다. Review는 author를 이기는 토론이 아니라 codebase가 시간이 지나도 maintainable하도록 만드는 feedback loop다. “완벽하지 않아도 전체 code health를 개선하면 approve할 수 있다”는 태도는 현실적인 engineering에 중요하다.
이 관점은 Clean Code의 엄격함과 A Philosophy of Software Design의 strategic thinking 사이에서 균형을 준다. 모든 CL에서 완벽한 architecture를 요구하면 progress가 막힌다. 하지만 code health를 악화시키는 shortcut을 계속 승인하면 system은 천천히 무너진다.
4.10 Language-specific guide
일반 원칙은 language마다 다르게 구현된다.
- C++에서는
RAII, value semantics, ownership, lifetime, exception safety,const,span,unique_ptr,shared_ptr선택이 design이다. - Rust에서는 ownership, borrowing, trait design, error type,
Send/Sync,unsafeboundary가 design이다. - Python에서는 readability, explicitness, module structure, duck typing, exception style, testability가 design이다.
- C에서는 resource ownership, error code convention, pointer lifetime, macro discipline, lock ordering, ABI stability가 design이다.
따라서 “좋은 code”는 universal rule과 language-specific idiom의 합이다.
5. Code 작성의 제1원칙: Change를 기준으로 설계하라
Code는 한 번 작성되고 끝나지 않는다. 대부분의 software cost는 initial implementation 이후에 발생한다. 따라서 좋은 code를 판단할 때는 현재 기능이 돌아가는지만 보면 안 된다. 다음 질문을 해야 한다.
- 이 feature가 조금 바뀌면 어디를 수정해야 하는가?
- 이 policy가 바뀌면 몇 개 module을 건드리는가?
- 이 data representation이 바뀌면 caller도 바뀌는가?
- 이 error case가 추가되면 main logic이 얼마나 흔들리는가?
- 이 code를 모르는 사람이 bug report를 받으면 어디서 시작할 수 있는가?
Good code는 future change의 shape를 예측한다. 모든 change를 예측할 수는 없다. 하지만 가능한 change category는 생각할 수 있다. 예를 들어 parser를 만들 때 format version이 바뀔 수 있고, allocator를 만들 때 allocation policy가 바뀔 수 있고, scheduler를 만들 때 priority rule이 바뀔 수 있다. 이런 design decision은 한곳에 모아야 한다.
5.1 Change axis를 찾는 법
새로운 module을 만들 때 다음을 구분하라.
- Stable concept: 오래 유지될 domain concept. 예:
Page,Inode,Transaction,Task,Session. - Volatile policy: 자주 바뀔 가능성이 있는 rule. 예: eviction policy, retry policy, scheduling priority, backoff strategy.
- Representation detail: 내부 layout이나 encoding. 예: byte order, file format, hash table layout.
- Integration detail: 외부 library, OS API, network protocol, device-specific behavior.
- Operational concern: logging, metrics, tracing, debug mode, failure injection.
좋은 design은 stable concept을 중심으로 interface를 만들고, volatile policy와 representation detail을 숨긴다.
5.2 Example: cache eviction
나쁜 design에서는 cache user가 eviction detail을 알고 있다.
fn read_page(cache: &mut Cache, key: PageId) -> Result<Page> {
if cache.len() > cache.max_entries() {
cache.remove_lru_entry();
}
if let Some(page) = cache.get(key) {
return Ok(page.clone());
}
let page = read_page_from_disk(key)?;
cache.insert(key, page.clone());
Ok(page)
}
여기서는 caller가 max_entries, LRU, insert timing을 안다. Eviction policy가 바뀌면 여러 caller가 흔들릴 수 있다.
더 나은 design은 cache가 policy를 내부에 숨긴다.
fn read_page(cache: &mut PageCache, key: PageId) -> Result<Page> {
cache.get_or_load(key, || read_page_from_disk(key))
}
이제 caller는 PageCache가 capacity와 eviction을 관리한다는 것만 안다. Cache module은 깊어진다. 내부는 더 복잡할 수 있지만 interface는 단순해진다.
6. Naming: 이름은 compressed design이다
이름은 단순한 label이 아니다. 좋은 이름은 reader에게 올바른 mental model을 준다. 나쁜 이름은 code를 읽기 전에 이미 잘못된 추측을 만들고, 그 추측을 고치기 위해 더 많은 context를 요구한다.
6.1 좋은 이름의 조건
좋은 이름은 다음을 만족한다.
- Precise: 너무 넓지도 좁지도 않다.
- Consistent: 같은 concept은 같은 단어로 부른다.
- Searchable: codebase에서 찾기 쉽다.
- Domain-aware: problem domain의 언어를 반영한다.
- Level-appropriate: abstraction level에 맞는다.
예를 들어 data, info, obj, manager, processor, handler는 자주 너무 넓다. 물론 handler가 정말 event handling abstraction이면 괜찮다. 문제는 이름이 책임을 숨기는 generic bucket이 되는 경우다.
6.2 이름은 type보다 먼저 읽힌다
다음 Rust code를 보자.
fn update(x: &mut Vec<u8>, y: usize) {
x[y] = 1;
}
Compiler는 이해하지만 사람은 이해하지 못한다. 다음은 훨씬 낫다.
fn mark_block_allocated(bitmap: &mut [u8], block_index: usize) {
bitmap[block_index] = 1;
}
여기서 이름은 data structure가 아니라 domain operation을 드러낸다. bitmap과 block_index는 reader가 code의 목적을 추측하게 돕는다.
6.3 Boolean 이름
Boolean은 특히 이름이 중요하다. flag, enabled, valid 같은 이름은 너무 일반적이다. Boolean 이름은 true일 때의 의미가 분명해야 한다.
bool dirty; // 무엇이 dirty인가?
bool page_has_unflushed_writes;
bool done; // 무엇이 끝났는가?
bool all_workers_stopped;
bool valid; // 어떤 validity인가?
bool header_checksum_matches_payload;
Boolean은 negation과 만나면 cognitive load가 커진다.
if not disable_cache:
use_cache()
가능하면 positive form을 쓰자.
if cache_enabled:
use_cache()
6.4 Naming consistency
같은 concept을 여러 단어로 부르면 information leakage가 생긴다.
user_id,uid,account_id가 같은가 다른가?block,page,frame이 같은가 다른가?task,thread,process,worker가 어떤 차이를 갖는가?
Systems code에서는 이런 차이가 매우 중요하다. page와 frame을 혼용하면 virtual memory와 physical memory concept이 섞인다. inode와 file을 혼용하면 metadata object와 opened file handle이 섞인다.
좋은 codebase는 glossary를 갖는다. 꼭 문서가 아니어도 된다. Type name, module name, public API name이 glossary 역할을 해야 한다.
7. Function design: 작은 function보다 coherent function
Function은 code의 가장 흔한 abstraction unit이다. 좋은 function은 caller에게 하나의 useful operation을 제공한다. 나쁜 function은 단지 line 수를 줄이기 위해 생긴 wrapper이거나, 여러 unrelated responsibility를 한곳에 몰아넣은 blob이다.
7.1 Function을 나누는 좋은 이유
Function을 분리할 때는 다음 중 하나 이상의 이유가 있어야 한다.
- 내부 detail을 숨긴다.
- 반복되는 design decision을 한곳으로 모은다.
- test하고 싶은 behavior boundary를 만든다.
- error handling이나 resource management를 명확히 한다.
- abstraction level을 맞춘다.
- concurrency/ownership invariant를 좁은 범위에 가둔다.
7.2 Function을 나누는 나쁜 이유
다음 이유만으로 나누면 shallow function이 될 가능성이 높다.
- “몇 줄 이상이면 무조건 나누자.”
- “모든 function은 한 screen 안에 들어와야 한다.”
- “이 code가 복잡해 보이니 이름으로 감추자.”
- “test coverage를 올리기 위해 private helper를 많이 만들자.”
복잡한 code에 이름을 붙인다고 복잡함이 사라지지는 않는다. 이름이 실제 abstraction을 제공할 때만 도움이 된다.
7.3 Single responsibility의 실용적 해석
“Function은 한 가지 일을 해야 한다”는 말은 유용하지만 모호하다. 한 가지 일이 무엇인지는 abstraction level에 따라 달라진다.
handle_page_fault()는 여러 일을 한다. page table을 확인하고, disk에서 page를 읽고, permission을 검사하고, TLB를 update할 수 있다. Low-level step으로 보면 여러 가지다. 하지만 OS 관점에서는 “page fault 처리”라는 coherent operation이다.
따라서 single responsibility는 “한 줄짜리 step”이 아니라 “하나의 reason to change”로 이해하는 편이 좋다.
int handle_page_fault(struct process *p, uintptr_t addr, enum fault_type type) {
struct vm_area *vma = find_vma(p, addr);
if (vma == NULL) return -EFAULT;
if (!fault_allowed(vma, type)) return -EACCES;
struct page *page = resolve_fault_page(p, vma, addr);
if (IS_ERR(page)) return PTR_ERR(page);
return install_pte(p, addr, page, vma->permissions);
}
이 function은 여러 step을 갖지만 story가 coherent하다. Helper들은 각각 meaningful abstraction이다. 반대로 각 line을 무조건 step1, step2처럼 나누면 reader는 더 힘들어진다.
7.4 Parameter design
Function parameter는 caller가 알아야 하는 정보다. Parameter가 많으면 caller의 cognitive load가 늘어난다.
Parameter가 많을 때는 다음을 의심하라.
- Function이 너무 많은 responsibility를 갖고 있는가?
- 함께 움직이는 parameter를 value object로 묶을 수 있는가?
- 일부 parameter는 module 내부 policy여야 하는가?
- boolean flag가 hidden mode를 만들고 있는가?
// 나쁜 예: caller가 너무 많은 policy를 안다.
read_file(path, true, false, 4096, 3, true);
// 더 나은 예: policy를 이름 있는 config로 묶는다.
ReadOptions options = {
.cache = CacheMode::Use,
.block_size = 4096,
.retry_count = 3,
.verify_checksum = true,
};
read_file(path, options);
Boolean flag parameter는 특히 조심하라.
render_user(user, True)
Reader는 True가 무엇인지 모른다. 다음이 낫다.
render_user(user, include_private_fields=True)
더 나아가 mode가 behavior를 크게 바꾸면 별도 function으로 나누는 편이 좋다.
render_public_user(user)
render_private_user(user)
8. Module과 API design: caller를 보호하라
Module은 code를 담는 상자가 아니라 complexity를 숨기는 장치다. 좋은 module은 내부가 복잡해도 interface가 단순하다. 나쁜 module은 내부도 얕고 interface도 많아서 caller가 대부분의 detail을 알아야 한다.
8.1 Deep module
Deep module은 simple interface 뒤에 많은 functionality를 제공한다. 예를 들어 Unix file I/O는 open, read, write, close 같은 비교적 단순한 interface로 file system, disk scheduling, buffer cache, permission, device driver detail을 숨긴다. Caller는 모든 detail을 몰라도 file을 사용할 수 있다.
Deep module의 장점은 cognitive load를 줄이는 것이다. Caller는 module contract만 이해하면 된다. 내부 implementation은 module author가 책임진다.
8.2 Shallow module
Shallow module은 interface를 배우는 비용에 비해 제공하는 value가 작다. 단순 wrapper, pass-through class, 이름만 다른 adapter가 여기에 해당한다.
class UserService:
def __init__(self, user_repository):
self.user_repository = user_repository
def get_user(self, user_id):
return self.user_repository.get_user(user_id)
이 class가 나쁜 것은 아니다. 미래에 authorization, caching, audit logging이 들어갈 clear boundary라면 유용할 수 있다. 하지만 그런 이유 없이 pass-through만 한다면 abstraction이 아니라 ceremony다.
8.3 API는 “무엇을 허용하지 않는가”까지 설계해야 한다
좋은 API는 가능한 operation뿐 아니라 불가능한 operation도 명확히 한다. 특히 systems code에서는 invalid state를 표현할 수 없게 만드는 것이 강력하다.
Rust 예시:
struct OpenFile {
fd: RawFd,
}
impl OpenFile {
fn read(&self, buf: &mut [u8]) -> io::Result<usize> { ... }
}
impl Drop for OpenFile {
fn drop(&mut self) {
unsafe { libc::close(self.fd); }
}
}
OpenFile type은 raw fd ownership을 감싼다. Caller는 close를 잊기 어렵고, ownership transfer가 type으로 표현된다. 이것은 style이 아니라 design이다.
C++에서는 RAII가 같은 역할을 한다.
class FileDescriptor {
public:
explicit FileDescriptor(int fd) : fd_(fd) {}
~FileDescriptor() { if (fd_ >= 0) ::close(fd_); }
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
FileDescriptor(FileDescriptor&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
private:
int fd_;
};
이 API는 resource lifetime decision을 caller에게 흩뿌리지 않는다.
8.4 Interface comment
Public API에는 comment가 필요하다. 특히 다음이 드러나야 한다.
- 이 function이 하는 일
- caller가 만족해야 하는 precondition
- return value의 의미
- ownership transfer 여부
- error condition
- concurrency rule
- performance expectation
/*
* Pins the page for DMA and returns a reference owned by the caller.
*
* The caller must release the page with unpin_dma_page(). This function may
* sleep and must not be called from interrupt context.
*/
struct page *pin_dma_page(struct device *dev, uintptr_t user_addr);
이 comment는 code의 중복이 아니라 interface contract다. 이런 정보가 없으면 caller는 implementation을 읽거나 다른 usage를 copy해야 한다.
9. Abstraction: detail을 지우는 것이 아니라 detail을 배치하는 것
Abstraction은 현실을 단순화한다. 하지만 좋은 abstraction은 거짓말하지 않는다. Detail을 숨기되, caller가 반드시 알아야 할 constraint는 contract로 드러낸다.
9.1 좋은 abstraction의 기준
좋은 abstraction은 다음을 만족한다.
- Caller가 common case를 쉽게 처리한다.
- Rare case가 common case를 오염시키지 않는다.
- Internal representation을 바꿔도 caller가 유지된다.
- Naming이 behavior expectation을 정확히 만든다.
- Performance나 failure semantics가 너무 놀랍지 않다.
9.2 나쁜 abstraction의 신호
- Caller가 call order를 외워야 한다.
- Function 이름은 단순하지만 hidden side effect가 많다.
- “이 경우에는 이 flag를 반드시 true로 넣어야 한다” 같은 rule이 많다.
- Module 내부 type이 public API에 새어 나온다.
- Error가 너무 generic해서 caller가 의미 있는 결정을 못 한다.
- Test가 implementation detail에 과도하게 묶인다.
9.3 Temporal decomposition
Temporal decomposition은 실행 순서에 따라 module을 나누는 실수다. 예를 들어 init, process, cleanup 같은 단계가 서로 강하게 같은 data와 policy를 공유한다면, phase별 module 분리는 information hiding에 실패할 수 있다.
나쁜 예:
config_loader.py # 모든 config를 읽음
config_validator.py # 모든 config를 검증함
config_applier.py # 모든 config를 적용함
이 구조에서는 특정 config option의 rule이 세 파일에 흩어질 수 있다. 새로운 option을 추가할 때 세 곳을 수정한다.
더 나은 구조:
storage_config.py
network_config.py
security_config.py
각 module이 자기 domain의 load/validate/apply를 책임지면 design decision이 함께 위치한다.
10. Comment와 documentation: code로 말할 수 없는 것을 말하라
Comment의 목적은 code를 반복하는 것이 아니다. Comment는 reader가 code만으로 알 수 없는 정보를 전달해야 한다.
10.1 좋은 comment의 종류
Interface comment
Public function, class, module의 contract를 설명한다.
/// Returns a snapshot of the current routing table.
///
/// The snapshot is consistent as of a single point in time, but it may be
/// stale immediately after this function returns. The function does not hold
/// the routing table lock after returning.
fn routing_table_snapshot(&self) -> RoutingTableSnapshot { ... }
Invariant comment
항상 유지되어야 하는 condition을 설명한다.
/*
* Invariant: free_list contains only blocks whose refcount is zero.
* A block may be visible in the hash table and the free_list at the same time,
* but only while it is clean and unpinned.
*/
Why comment
왜 이 방식이 선택되었는지 설명한다.
// We intentionally avoid std::unordered_map here: iteration order must be
// stable across runs because the serialized output is part of the cache key.
std::map<Key, Value> entries;
Nonobvious consequence
겉보기와 다른 중요한 consequence를 설명한다.
/*
* This write barrier pairs with the acquire load in wake_waiters(). Without it,
* a waiter can observe the wakeup flag before the queue update is visible.
*/
smp_store_release(&state->ready, true);
10.2 나쁜 comment의 종류
- Code를 그대로 반복한다.
- 오래되어 현재 behavior와 다르다.
- 애매한 변명만 한다.
- 중요한 contract는 빼고 implementation detail만 설명한다.
- “TODO”가 issue tracking 없이 영구히 남아 있다.
# bad: user를 가져온다
user = get_user(user_id)
이 comment는 지워도 된다.
10.3 Comment를 줄이는 법
Comment가 너무 많으면 다음 중 하나일 수 있다.
- Code가 너무 nonobvious하다.
- Name이 부정확하다.
- Function이 여러 abstraction level을 섞는다.
- Interface가 caller에게 너무 많은 detail을 요구한다.
- Invariant가 type이나 API로 표현되지 않았다.
이 경우 comment를 삭제하기보다 먼저 code를 개선하라. 하지만 code로 표현할 수 없는 design rationale까지 없애면 안 된다.
11. Error handling: happy path만 설계하지 말라
Error handling은 code quality의 핵심이다. 많은 codebase에서 main logic은 깔끔하지만 error path가 복잡하고 불안정하다. Systems code에서는 error path가 resource leak, double free, deadlock, inconsistent state를 만든다.
11.1 Error를 policy와 mechanism으로 나눠라
Error handling에는 두 층이 있다.
- Mechanism: error를 표현하고 전달하는 방식. 예:
errno,Result<T, E>, exception, status code. - Policy: error가 발생했을 때 무엇을 할지. 예: retry, abort, fallback, log, user notification.
나쁜 code는 low-level module이 policy까지 결정한다.
fn read_config(path: &Path) -> Config {
match std::fs::read_to_string(path) {
Ok(s) => parse_config(&s),
Err(_) => std::process::exit(1),
}
}
Library function이 process를 exit하면 caller는 선택권을 잃는다. 더 나은 code는 error를 전달한다.
fn read_config(path: &Path) -> Result<Config, ConfigError> {
let s = std::fs::read_to_string(path)?;
parse_config(&s)
}
Policy는 application boundary에서 결정한다.
11.2 Error type은 caller decision을 도와야 한다
Error가 너무 generic하면 caller가 의미 있는 처리를 못 한다.
raise Exception("failed")
더 나은 error는 domain decision을 가능하게 한다.
class ConfigError(Exception):
pass
class MissingConfig(ConfigError):
pass
class InvalidConfigSyntax(ConfigError):
pass
하지만 error type을 지나치게 많이 만들면 또 다른 complexity가 된다. 기준은 caller가 다르게 처리할 필요가 있는가다.
11.3 Cleanup path는 structure로 보장하라
C에서는 goto cleanup이 오히려 clean할 때가 많다.
int setup_device(struct device *dev) {
int ret;
ret = alloc_buffers(dev);
if (ret) return ret;
ret = register_irq(dev);
if (ret) goto free_buffers;
ret = start_device(dev);
if (ret) goto unregister_irq;
return 0;
unregister_irq:
unregister_irq(dev);
free_buffers:
free_buffers(dev);
return ret;
}
중요한 것은 cleanup order가 acquisition order의 reverse로 명확하다는 점이다. C++/Rust에서는 RAII/Drop으로 이 구조를 type에 넣을 수 있다.
12. State와 mutability: complexity의 주범을 좁혀라
State는 software에서 가장 큰 complexity source 중 하나다. State 자체가 나쁜 것은 아니다. OS, database, cache, protocol, UI는 모두 state를 다룬다. 문제는 state가 어디서 바뀌는지, 어떤 invariant가 있는지, 누가 소유하는지 모를 때 생긴다.
12.1 State를 줄이는 방법
- Derived state를 저장하지 말고 계산하라.
- Mutable global state를 피하라.
- State transition을 explicit하게 만들어라.
- State owner를 하나로 정하라.
- Invariant를 type/API로 표현하라.
- Snapshot과 live view를 구분하라.
12.2 State machine으로 생각하기
복잡한 lifecycle은 boolean 여러 개보다 state machine이 낫다.
나쁜 예:
bool initialized;
bool running;
bool stopping;
bool stopped;
이 조합은 invalid state를 만들 수 있다. running == true와 stopped == true가 동시에 가능해진다.
더 나은 예:
enum class WorkerState {
Created,
Initialized,
Running,
Stopping,
Stopped,
};
Rust라면 type system으로 transition을 더 강하게 표현할 수 있다.
struct CreatedWorker { ... }
struct RunningWorker { ... }
impl CreatedWorker {
fn start(self) -> io::Result<RunningWorker> { ... }
}
이 design은 invalid operation을 compile time에 줄인다.
12.3 Ownership
Ownership은 “누가 free하는가”만의 문제가 아니다. 누가 update할 수 있는지, 누가 observe할 수 있는지, 언제 lifetime이 끝나는지에 대한 design이다.
C에서는 ownership convention을 comment와 naming으로 명확히 해야 한다.
/* Returns a newly allocated buffer. Caller owns the returned buffer. */
char *read_file_to_buffer(const char *path);
/* Borrows buf for the duration of the call. Does not retain the pointer. */
int parse_header(const char *buf, size_t len, struct header *out);
Rust에서는 ownership이 type system에 들어가지만, API author는 여전히 어떤 ownership model이 reader에게 단순한지 결정해야 한다.
13. Concurrency: interleaving을 design 밖으로 새게 하지 말라
Concurrency는 complexity를 폭발시키기 쉽다. 두 execution flow가 같은 state를 읽고 쓰면 가능한 interleaving 수가 급격히 늘어난다. 좋은 concurrent code는 가능한 interleaving을 줄이고, 남은 interleaving의 rule을 명확히 한다.
13.1 Concurrency design 질문
- Shared state는 무엇인가?
- 누가 write할 수 있는가?
- Lock ordering은 무엇인가?
- Atomic operation의 memory ordering은 무엇인가?
- Callback은 lock을 잡은 상태에서 호출되는가?
- Blocking operation은 어디서 허용되는가?
- Cancellation은 어느 지점에서 처리되는가?
이 질문의 답이 code에 흩어져 있으면 reader는 매우 힘들어진다.
13.2 Lock scope를 작게, invariant를 크게
Lock scope는 작을수록 좋지만, invariant가 깨지는 구간이 많아지면 오히려 어렵다. 중요한 것은 lock이 보호하는 invariant를 명확히 하는 것이다.
/*
* queue_lock protects both wait_queue and queued_count.
* queued_count must equal the number of nodes reachable from wait_queue.
*/
struct wait_queue {
spinlock_t queue_lock;
struct list_head wait_queue;
size_t queued_count;
};
Lock이 무엇을 보호하는지 명확히 쓰면 future change가 안전해진다.
13.3 Message passing과 ownership transfer
Shared mutable state를 줄이는 한 방법은 message passing이다. Worker가 data ownership을 넘겨받고, 처리 후 결과를 돌려준다. 이 방식은 state ownership을 좁혀 reasoning을 쉽게 만든다.
하지만 message passing도 만능은 아니다. Queue backpressure, cancellation, ordering, failure semantics를 설계해야 한다. 단순히 lock을 channel로 바꾼다고 complexity가 사라지지 않는다. Complexity는 다른 곳으로 이동한다.
14. Testing: design feedback이자 change insurance
Test는 bug를 찾는 도구이기도 하지만, 더 중요하게는 change를 가능하게 하는 insurance다. Test가 없으면 developer는 refactoring을 두려워한다. 두려움은 shortcut을 낳고, shortcut은 complexity를 키운다.
14.1 좋은 test의 조건
- 중요한 behavior를 보호한다.
- Failure message가 원인을 좁힌다.
- Implementation detail에 과도하게 묶이지 않는다.
- Deterministic하다.
- 빠르게 실행된다.
- Edge case와 error path를 포함한다.
14.2 Test가 design을 압박한다
Test하기 어려운 code는 design smell일 수 있다. 물론 모든 code가 쉽게 unit test될 수는 없다. Kernel, distributed system, embedded system은 test 환경이 어렵다. 하지만 dependency를 주입할 seam이 없고, state가 global에 흩어져 있고, timing에 강하게 의존한다면 design을 의심해야 한다.
나쁜 예:
def sync_users():
users = requests.get("https://example.com/users").json()
db = connect_to_prod_db()
for user in users:
db.upsert(user)
Network, database, production config가 function 내부에 박혀 있다. Test가 어렵다.
더 나은 예:
def sync_users(fetch_users, user_store):
for user in fetch_users():
user_store.upsert(user)
이제 core behavior는 fake dependency로 test할 수 있다. Production wiring은 바깥에서 한다.
14.3 TDD에 대한 균형
Clean Code 계열은 TDD를 강하게 강조한다. TDD는 API를 caller 관점에서 생각하게 만들고, 빠른 feedback을 제공하며, refactoring safety를 준다. 하지만 모든 상황에서 strict TDD가 최선은 아니다. Exploratory algorithm, performance tuning, low-level hardware interaction, research prototype에서는 먼저 실험 code가 필요할 수 있다.
중요한 것은 test-first라는 ritual보다 feedback loop다.
- Behavior를 어떻게 검증할 것인가?
- Refactoring 전에 무엇을 보호해야 하는가?
- Bug가 재발하지 않게 어떤 regression test를 추가할 것인가?
- Test가 design을 더 단순하게 만들고 있는가, 더 brittle하게 만들고 있는가?
15. Refactoring: design을 회복하는 기술
Refactoring은 feature 개발과 별개인 luxury가 아니다. Refactoring은 future feature를 가능하게 하는 투자다. 하지만 무작정 “정리”하는 것은 위험하다. 좋은 refactoring은 목적이 분명하고, 작고, behavior-preserving이며, test나 review로 보호된다.
15.1 Refactoring의 기본 순서
- 변경하고 싶은 behavior boundary를 찾는다.
- 현재 behavior를 test로 고정한다.
- 작은 transformation을 적용한다.
- Test를 실행한다.
- Naming과 interface를 정리한다.
- 다음 작은 transformation으로 이동한다.
15.2 큰 rewrite를 피해야 하는 이유
큰 rewrite는 매력적이다. 기존 complexity를 버리고 새로 시작할 수 있을 것 같다. 하지만 rewrite는 기존 system에 숨어 있던 requirement를 잃기 쉽다. Legacy code의 이상한 branch는 과거 장애, 고객 요구, platform quirk, performance workaround의 흔적일 수 있다.
따라서 rewrite보다 incremental refactoring이 안전한 경우가 많다. 다만 다음 경우에는 rewrite가 합리적일 수 있다.
- 기존 system의 requirement를 충분히 capture했다.
- 새 implementation을 parallel run으로 검증할 수 있다.
- Interface boundary가 명확하다.
- Migration plan과 rollback plan이 있다.
- 기존 code가 change를 거의 불가능하게 만들 정도로 심각하다.
15.3 Refactoring target을 고르는 법
Refactoring은 가장 보기 싫은 code부터 하는 것이 아니다. Impact가 큰 곳부터 한다.
- 자주 바뀌는 code
- bug가 자주 나는 code
- 여러 feature가 의존하는 boundary
- onboarding을 막는 code
- test가 없어 변경을 두렵게 만드는 code
- duplicated design decision이 퍼진 code
아름답지만 거의 수정되지 않는 code보다, 매주 사람을 괴롭히는 평범한 code가 더 좋은 target이다.
16. Legacy code: 미워하지 말고 먼저 붙잡아라
Legacy code는 나쁜 사람이 쓴 code가 아니다. Legacy code는 시간, 요구사항, 조직 변화, schedule pressure, tool limitation, knowledge loss가 쌓인 결과다. 좋은 태도는 비난이 아니라 복원이다.
16.1 Legacy code를 만났을 때
- 바로 고치지 말고 behavior를 관찰한다.
- Characterization test를 추가한다.
- 위험한 dependency를 찾는다.
- Seam을 만든다.
- 작은 refactoring으로 structure를 드러낸다.
- 새 feature를 넣기 전에 change point를 좁힌다.
16.2 Characterization test
Characterization test는 “올바른 behavior”를 증명하는 test가 아니라 “현재 behavior”를 기록하는 test다. Legacy code에서는 현재 behavior 자체가 requirement일 수 있다.
def test_legacy_parser_accepts_empty_field_before_checksum():
# This behavior is surprising, but existing clients depend on it.
result = parse_packet("user=alice;;checksum=0")
assert result.fields[1] == ""
이 test는 이상한 behavior를 정당화하지 않는다. 변경 전에 그 behavior가 존재함을 기록한다. 이후 product decision으로 바꿀 수 있다.
16.3 Seam 만들기
Seam은 test와 change를 가능하게 하는 접점이다.
나쁜 예:
int send_report() {
Socket s("prod.example.com", 443);
auto data = collect_report();
return s.send(data);
}
더 나은 예:
class ReportSink {
public:
virtual ~ReportSink() = default;
virtual int send(std::string_view data) = 0;
};
int send_report(ReportSink& sink) {
auto data = collect_report();
return sink.send(data);
}
이제 test에서는 fake sink를 넣을 수 있다. 다만 모든 dependency를 virtual interface로 바꾸는 것은 과하다. Seam은 change/test 필요가 있는 곳에 만든다.
17. Code review: 완벽함보다 code health
Code review는 bug finding만이 아니다. Code review는 design knowledge를 공유하고, codebase의 consistency를 유지하고, future maintainer를 보호하는 과정이다.
17.1 Reviewer가 봐야 할 것
- Design이 system에 맞는가?
- Functionality가 의도대로 동작하는가?
- Complexity를 줄일 수 있는가?
- Test가 적절한가?
- Naming이 명확한가?
- Comment가 useful한가?
- Style guide를 따르는가?
- Documentation이 필요한가?
- Security/performance/concurrency issue가 있는가?
17.2 좋은 review comment
좋은 review comment는 구체적이고, 이유가 있고, 중요도를 표현한다.
이 function은 `retry_count`와 `backoff_ms`를 caller가 직접 계산하게 해서 retry policy가 세 곳에 퍼질 것 같아요. `RetryPolicy`를 module 내부로 넣으면 change point가 줄어들 것 같습니다.
나쁜 comment:
이거 별로네요.
좋은 reviewer는 취향과 principle을 구분한다. Style guide가 정한 것은 style guide를 따르면 된다. Design issue는 underlying principle로 설명해야 한다.
17.3 Author가 해야 할 것
- PR description에 design intent를 쓴다.
- 왜 이 approach를 택했는지 설명한다.
- Alternative를 고려했다면 간단히 적는다.
- Risk와 test coverage를 밝힌다.
- 큰 change는 작은 commit으로 나눈다.
- Review comment를 방어적으로 받아들이지 않는다.
좋은 PR은 reviewer가 code archaeologist가 되지 않게 한다.
18. Language-specific notes
18.1 C
C에서 clean code는 readability만으로 부족하다. Ownership, lifetime, error path, macro, pointer aliasing, alignment, integer overflow, concurrency가 핵심이다.
C checklist
- 누가 allocation을 소유하는가?
- Caller가 free해야 하는가?
- Borrowed pointer가 call 이후에도 저장되는가?
NULL이 허용되는가?- Buffer length가 항상 함께 전달되는가?
- Error code convention이 일관되는가?
- Cleanup path가 acquisition reverse order인가?
- Macro가 side effect를 두 번 평가하지 않는가?
- Lock ordering이 문서화되어 있는가?
#define MIN_BAD(a, b) ((a) < (b) ? (a) : (b))
이 macro는 argument side effect를 두 번 평가할 수 있다. 가능하면 static inline을 쓰거나 side effect 없는 expression만 받는 convention을 명확히 하라.
18.2 C++
C++에서는 RAII와 value semantics가 design의 중심이다. Raw pointer는 non-owning인지 owning인지 모호하다. unique_ptr, shared_ptr, reference, value, span 등을 통해 intent를 표현해야 한다.
C++ checklist
- Resource는 RAII object가 소유하는가?
- Copy/move semantics가 명확한가?
const가 reader에게 stability를 알려주는가?- Exception safety guarantee가 있는가?
- Template abstraction이 readability를 해치지 않는가?
shared_ptr가 ownership을 흐리지 않는가?- Virtual dispatch가 필요한가, 아니면 static polymorphism이 나은가?
18.3 Rust
Rust의 type system은 많은 invariant를 표현하게 해준다. 하지만 Rust code도 복잡해질 수 있다. Lifetime gymnastics, over-generic API, trait explosion, unnecessary Arc<Mutex<T>>, careless unwrap, broad error type은 cognitive load를 키운다.
Rust checklist
- Ownership model이 API에서 자연스러운가?
Resulterror type이 caller decision을 돕는가?unsafeboundary가 작고 문서화되어 있는가?Arc<Mutex<T>>가 정말 필요한가?- Trait은 real abstraction인가, 단지 test를 위한 ceremony인가?
- Generic parameter가 signature를 과도하게 복잡하게 만드는가?
- Public API가 future compatibility를 고려하는가?
18.4 Python
Python에서는 readability와 explicitness가 특히 중요하다. Dynamic typing은 빠른 개발을 돕지만, 큰 codebase에서는 type hint, clear module boundary, test가 중요해진다.
Python checklist
- Function 이름이 side effect를 드러내는가?
- Mutable default argument를 피했는가?
- Exception type이 구체적인가?
- Module import가 순환 dependency를 만들지 않는가?
- Type hint가 reader에게 도움을 주는가?
- Test가 monkeypatch 남용으로 brittle하지 않은가?
- Data model을
dataclass나pydantic같은 구조로 표현할 필요가 있는가?
# 나쁜 예
def add_item(item, items=[]):
items.append(item)
return items
# 더 나은 예
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
19. LLM-assisted coding: LLM을 code generator가 아니라 reviewer로 써라
LLM은 code 작성 속도를 높일 수 있지만, complexity를 자동으로 줄여주지는 않는다. LLM이 만든 code는 그럴듯한 naming과 formatting을 가질 수 있지만, boundary decision, hidden invariant, error path, concurrency rule이 부실할 수 있다.
19.1 좋은 사용법
- “이 API의 caller cognitive load를 줄이는 대안을 제시해줘.”
- “이 code에서 hidden state와 side effect를 찾아줘.”
- “이 function을 작은 helper로 나누되 shallow wrapper를 만들지 않게 해줘.”
- “이 module의 invariant를 interface comment로 정리해줘.”
- “이 error handling path에서 resource leak 가능성을 찾아줘.”
- “이 code를 APoSD 관점과 Clean Code 관점으로 각각 review해줘.”
19.2 위험한 사용법
- 이해하지 못한 code를 그대로 붙여넣기
- Test 없이 generated refactoring 적용하기
- Library/API semantics 확인 없이 사용하기
- Security-critical code를 review 없이 생성하기
- Concurrency code를 model 없이 생성하기
- “깔끔해 보이는” code를 correctness보다 우선하기
19.3 LLM에게 줄 좋은 prompt
다음 Rust module을 review해줘.
관점은 다음 순서로 봐줘.
1. APoSD: complexity, information hiding, shallow/deep module, interface cognitive load
2. Clean Code: naming, function responsibility, duplication, testability
3. Systems concerns: ownership, error path, concurrency, performance invariant
4. 제안은 큰 rewrite보다 작은 refactoring step으로 제시해줘.
LLM은 혼자 쓰면 hallucination할 수 있다. 하지만 좋은 질문과 source code, test result, constraint를 함께 주면 매우 강한 rubber duck과 reviewer가 된다.
20. 실전 checklist
20.1 새 function 작성 전
- 이 function의 caller는 누구인가?
- Caller가 무엇을 몰라도 되게 만들 것인가?
- 이름만 보고 behavior를 예측할 수 있는가?
- Parameter는 모두 caller가 알아야 하는 정보인가?
- Error는 어떻게 표현할 것인가?
- Resource ownership은 어떻게 이동하는가?
- Test는 어떤 behavior를 보호할 것인가?
20.2 새 module 작성 전
- 이 module이 숨길 design decision은 무엇인가?
- Public interface는 최소한인가?
- 내부 representation이 새어 나오지 않는가?
- Common case가 쉬운가?
- Rare case가 common case를 오염시키지 않는가?
- Extension point가 실제 change axis에 맞는가?
- Documentation 없이 사용할 수 있는가? Documentation이 필요한 곳은 contract인가?
20.3 Code review 중
- 이 change가 전체 code health를 개선하는가?
- 나쁜 abstraction을 추가하지 않는가?
- Duplication은 같은 knowledge의 duplication인가, 우연한 similarity인가?
- Test가 meaningful한가?
- Error path가 main path만큼 안전한가?
- Comment는 code를 반복하는가, hidden knowledge를 보존하는가?
- Future maintainer가 이 decision의 이유를 알 수 있는가?
20.4 Refactoring 전
- 무엇을 더 쉽게 만들기 위한 refactoring인가?
- 현재 behavior를 test로 붙잡았는가?
- 한 번에 너무 많이 바꾸고 있지 않은가?
- Refactoring 후 interface가 더 단순해지는가?
- Change amplification이 줄어드는가?
- Cognitive load가 줄어드는가?
21. 좋은 code의 red flags
다음 신호가 보이면 멈춰서 design을 다시 보자.
21.1 Naming red flags
manager,processor,handler,data,info가 남용된다.- 같은 concept에 여러 이름이 있다.
- Boolean이 negation으로 읽힌다.
- 이름이 implementation detail에 묶여 있다.
- 이름이 너무 generic해서 검색이 어렵다.
21.2 Function red flags
- Parameter가 많다.
- Boolean flag로 mode가 갈린다.
- Caller가 call order를 외워야 한다.
- Helper가 pass-through만 한다.
- Error handling이 main logic보다 복잡하지만 structure가 없다.
- Function이 여러 reason to change를 가진다.
21.3 Module red flags
- Public API가 internal type을 노출한다.
- Common case 사용법이 길다.
- Configuration이 caller마다 반복된다.
- Wrapper가 많지만 실제 behavior는 없다.
- 같은 policy가 여러 module에 흩어져 있다.
- Documentation이 없으면 사용할 수 없는데, 그 이유가 hidden contract 때문이다.
21.4 Test red flags
- Test가 implementation detail을 그대로 따라간다.
- Mock이 너무 많아 behavior보다 interaction만 검증한다.
- Test 이름이 무엇을 보호하는지 말하지 않는다.
- Flaky test가 방치된다.
- Error path test가 없다.
- Refactoring만 해도 test가 대량으로 깨진다.
22. 책별로 무엇을 흡수할 것인가
22.1 A Philosophy of Software Design에서 흡수할 것
- Complexity가 enemy라는 관점
- Deep module
- Information hiding
- Strategic programming
- Pull complexity downward
- Comment as design documentation
- Red flag를 통한 design review
주의할 점: 너무 추상적으로만 읽으면 실제 coding habit이 바뀌지 않는다. 읽은 뒤 자기 code에서 shallow module과 information leakage를 찾아야 한다.
22.2 Clean Code에서 흡수할 것
- Naming discipline
- Function/class hygiene
- Boy Scout Rule
- Test discipline
- Smell을 감지하는 감각
- Reader 중심의 code 작성 태도
주의할 점: 작은 function, comment minimization, strict rule을 절대화하면 fragmentation이 생길 수 있다. Rule보다 reader의 cognitive load를 기준으로 삼아야 한다.
22.3 Refactoring에서 흡수할 것
- 작은 behavior-preserving step
- Code smell vocabulary
- Test와 refactoring의 결합
- Design은 working code 위에서 개선될 수 있다는 태도
주의할 점: Refactoring은 목적 없이 하면 churn이 된다. 어떤 change를 쉽게 만들려는지 분명해야 한다.
22.4 Working Effectively with Legacy Code에서 흡수할 것
- Seam 찾기
- Characterization test
- Dependency breaking
- Untested code를 안전하게 바꾸는 순서
주의할 점: 모든 것을 test double과 interface로 바꾸면 complexity가 늘 수 있다. Seam은 필요한 곳에 만든다.
22.5 The Pragmatic Programmer에서 흡수할 것
- Responsibility
- Feedback loop
- Automation
- Tool mastery
- DRY를 knowledge duplication 관점으로 보기
- Prototype과 tracer bullet 사고
주의할 점: 넓은 조언이 많으므로, 읽고 감탄하는 데서 끝내지 말고 개인 workflow에 checklist로 넣어야 한다.
22.6 Code Complete에서 흡수할 것
- Construction practice의 폭
- Defensive programming
- Debugging과 quality control
- Detailed design과 coding의 연결
주의할 점: 일부 예시는 오래되었을 수 있으므로 language-specific modern guideline과 함께 읽어야 한다.
22.7 Design Patterns에서 흡수할 것
- Pattern vocabulary
- Recurring design trade-off
- Interface와 implementation 분리
- Composition과 delegation 사고
주의할 점: Pattern을 적용하는 것이 목표가 아니다. Pattern은 문제에 맞을 때만 사용한다.
23. 추천 reading path
23.1 빠르게 code quality 감각을 만들고 싶을 때
A Philosophy of Software DesignClean CodeRefactoring- Google Engineering Practices code review guide
이 순서는 design philosophy → local hygiene → safe transformation → team practice로 이어진다.
23.2 Legacy code를 많이 다룰 때
Working Effectively with Legacy CodeRefactoringA Philosophy of Software DesignCode Complete
먼저 test와 seam을 배워야 한다. 그다음 design을 개선한다.
23.3 Systems code를 잘 쓰고 싶을 때
A Philosophy of Software DesignCode Complete- C++ Core Guidelines 또는 Rust API Guidelines
No Silver Bullet,Out of the Tar Pit,Simple Made Easy- 좋은 OS/kernel/database codebase의 style guide와 code review 사례
Systems code에서는 abstraction이 성능, lifetime, concurrency, hardware/API constraint와 함께 움직인다. 일반 clean code 원칙을 그대로 적용하기보다 systems-specific invariant를 중심에 두어야 한다.
23.4 Python 중심 application code를 잘 쓰고 싶을 때
- PEP 8과 PEP 20
Clean CodeRefactoringThe Pragmatic ProgrammerA Philosophy of Software Design
Python은 빠르게 작성하기 쉬운 만큼 module boundary와 explicitness가 중요하다.
24. 최종 원칙: 좋은 code는 kindness다
좋은 code는 미래의 reader에게 친절하다. 여기서 친절함은 주석을 많이 다는 것이 아니다. 친절함은 reader가 잘못된 추측을 하지 않도록 structure를 만드는 것이다.
좋은 code는 다음을 제공한다.
- 이름을 통해 목적을 알려준다.
- Interface를 통해 detail을 숨긴다.
- Type과 API를 통해 invalid state를 줄인다.
- Comment를 통해 hidden invariant를 보존한다.
- Test를 통해 change를 두려워하지 않게 한다.
- Refactoring을 통해 design debt를 갚는다.
- Review를 통해 team knowledge를 공유한다.
결국 좋은 code 작성법은 하나의 문장으로 돌아온다.
내가 지금 아는 것을 미래의 reader가 몰라도 안전하게 작업할 수 있도록 만들어라.
이것이 Clean Code의 readability, A Philosophy of Software Design의 complexity reduction, Refactoring의 safe change, Working Effectively with Legacy Code의 test seam, Pragmatic Programmer의 responsibility가 만나는 지점이다.
References
이 문서는 아래 자료들을 바탕으로 한 synthesis이다. 일부 설명은 각 자료의 공식 소개 페이지와 publicly available article/guide를 참고했다.
- [S1] John Ousterhout, A Philosophy of Software Design — Software Design Book. Official Stanford page. https://web.stanford.edu/~ouster/cgi-bin/book.php
- [S2] Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, 1st edition. Pearson. https://www.pearson.com/en-us/subject-catalog/p/clean-code-a-handbook-of-agile-software-craftsmanship/P200000009044/9780136083252
- [S3] Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, 2nd edition. Pearson. https://www.pearson.com/en-us/subject-catalog/p/clean-code-a-handbook-of-agile-software-craftsmanship-2nd-edition/P200000013239/9780135398548
- [S4] Martin Fowler, Refactoring: Improving the Design of Existing Code. https://martinfowler.com/books/refactoring.html
- [S5] Andrew Hunt and David Thomas, The Pragmatic Programmer, 20th Anniversary Edition. Pragmatic Bookshelf. https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/
- [S6] Steve McConnell, Code Complete, 2nd Edition. Microsoft Press Store. https://www.microsoftpressstore.com/store/code-complete-9780735619678
- [S7] Michael Feathers, Working Effectively with Legacy Code. InformIT. https://www.informit.com/store/working-effectively-with-legacy-code-9780131177055
- [S8] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software. InformIT. https://www.informit.com/store/design-patterns-elements-of-reusable-object-oriented-9780201633610
- [S9] David L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules. Semantic Scholar entry. https://www.semanticscholar.org/paper/On-the-criteria-to-be-used-in-decomposing-systems-Parnas/877e314d3a9f9317c162309c9ee0c660878a4bdb
- [S10] Frederick P. Brooks, No Silver Bullet: Essence and Accidents of Software Engineering. Semantic Scholar entry. https://www.semanticscholar.org/paper/No-Silver-Bullet-Essence-and-Accidents-of-Software-Brooks/c1e428890375e8deb183270aa31ac203d561abae
- [S11] Ben Moseley and Peter Marks, Out of the Tar Pit. Semantic Scholar entry. https://www.semanticscholar.org/paper/Out-of-the-Tar-Pit-Moseley-Marks/41dc590506528e9f9d7650c235b718014836a39d
- [S12] Rich Hickey, Simple Made Easy. InfoQ presentation page. https://www.infoq.com/presentations/Simple-Made-Easy/
- [S13] Google, Engineering Practices Documentation: Code Review. https://google.github.io/eng-practices/review/
- [S14] Google, The Standard of Code Review. https://google.github.io/eng-practices/review/reviewer/standard.html
- [S15] ISO C++ community, C++ Core Guidelines. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
- [S16] Rust API Guidelines Working Group, Rust API Guidelines Checklist. https://rust-lang.github.io/api-guidelines/checklist.html
- [S17] Python Software Foundation, PEP 8 — Style Guide for Python Code. https://peps.python.org/pep-0008/
- [S18] Python Software Foundation, PEP 20 — The Zen of Python. https://peps.python.org/pep-0020/