개발에 대해 아무것도 모를 때 읽었던 Clean Code와 어느 정도 경험을 쌓고 나서 다시 보는 Clean Code는 느낌이 달랐다.
직접 프로그램을 유지보수하지 않고 개발만 했던 시절에는 모든 내용이 내 머리속에 있었기 때문에 "대체 왜 interface를 사용하는 거지? 굳이? 그냥 수정하면 되는 거 아닌가?"라고 생각했다. 지나고 보니 우매한 생각이었다. 여러 프로그램을 유지보수하다보니 요구사항을 반영할 때 기존 코드를 최소한으로 만져야 하고, dependency가 꼬여있지 않아 추가하고 싶은 기능만 추가하는 것의 중요성을 깨달았다. 기존 코드가 난잡하면 유지보수 하는데는 훨씬 많은 cost가 든다는 걸 개발병을 하면서 너무 많이 겪었다.
나는 소스 코드는 하나의 글이라 생각한다. 잘 쓴 글은 서두만 봐도 무슨 내용인지 예상되며, 문맥상 어색한 내용을 작성하지 않는다. 또한 한 문단에서 한 주장만을 다루며, (강조를 제외하고는) 같은 말을 반복하지 않는다. 이를 Clean Code식으로 바꾼다면 다음과 같다.
- 서두만 봐도 무슨 내용인지 예상된다 → abstraction과 naming이 잘 되어 있다.
- 문맥상 어색한 내용을 작성하지 않는다 → 이것저것 꼬여있지 않고 명확한 hierarchy가 있다.
- 한 문단에서 한 주장만을 다룬다 → 한 class는 한 역할만을 하고(SRP), 한 function은 한 기능만을 한다.
- 같은 말을 반복하지 않는다 → 중복이 없다.
이외에도 당연한 이야기를 많이 설명하는데, 이 중에서 내게 필요하다고 생각되는 항목들만 매우 간략히 정리했다.
2장. 의미 있는 이름
소스 코드는 나만 읽는 글이 아니다. 나만 알아볼 수 있는 이름이 아니라 누가 봐도 알아볼 수 있는 이름을 작성하자는 게 이 장의 핵심이다.
- 모든 함수, 변수, 상수는 이름만 보고 무슨 역할을 하는지 알 수 있어야 한다.
- Class 이름은 명사, 함수 이름은 동사로 사용해야 한다.
- 같은 개념은 같은 단어로 나타내야 한다.
- 예를 들어 [어떤 정보를 가져오는 method 이름]을 get, retrieve, fetch 등으로 분산시키지 말고 하나로 통일해야 한다. 가능하면 많은 사람들이 이해하고 있는 이름을 선정해야 한다.
3장. 함수
작게 만들어야 한다. 긴 함수는 low level abstraction과 high level abstraction이 혼재되어 있어 이해하기 어렵다. 또한 Single Responsibility Principle와 동일한 이유로 로직의 자그마한 부분이 바뀌어도 해당 method를 고쳐야 하기에 side-effect가 발생할 확률이 높아진다. 이 장에서는 함수를 작게 만드는 방법을 기술한다.
- 함수는 하나의 역할만 해야 한다. 따라서 함수를 작게 만들어야 한다.
- Clean Code에서는 추상화 수준이 하나일 때 한 가지 작업만 한다고 판정한다. 또는 더 이상 의미 있는 이름으로 함수를 추출할 수 없을 때 한 가지 작업만 한다고 판정한다.
- 함수 이름에 맞는 딱 한 가지의 일만 하고 나머지 작업을 몰래 하는 경우는 없어야 한다.
- abstraction level이 높은 것은 위로, 낮은 것은 아래로 내려야 한다.
- parameter는 최대한 줄일수록 좋다. parameter가 많아질수록 test code를 짜기 힘들어지기 때문이다.
- parameter에 출력 변수를 사용하지 않아야 한다. 직관적이지 않기 때문이다.
- 오류 코드 리턴보다 try-catch가 낫다.
- 오류 코드 리턴은 결과로 if-else처리를 해야 한다. try-catch는 별도 함수로 뽑아내 묶을 수 있다.
4장. 주석
주석은 관리 대상이 아니기 때문에 방치될 확률이 높고, 따라서 주석은 최대한 줄여야 한다는 것이 이 장의 핵심이다.
- 주석보다는 Code로 설명해야 한다.
- 주석으로 설명하는 대신 적절한 naming으로 설명해야 한다.
- 단, 다음과 같은 몇 가지는 예외이다.
- 저작권, 소유권 등 법적 주석
- 정규표현식 등 직관적으로 이해하기 힘든 코드에 대한 주석
- 의도를 설명하는 주석: 알고리즘 동작 방식 등
- 경고하는 주석
- TODO 주석
- 위 경우를 제외하고는 주석을 최대한 지양한다.
5장. 형식
글 내용은 동일하더라도 들여쓰기를 하는지, 문단 분리를 적절히 하는지에 따라 가독성이 크게 달라진다. 소스코드도 동일하다. 이 장에서는 코드가 지키면 좋은 형식을 다룬다.
- 팀이 정한 규칙이 언제나 1순위이다.
- 각 개념은 개행으로 구분한다.
- 개행은 개념의 분리를 나타낸다. package - import - method 등은 개행으로 분리해야 한다.
- caller function은 callee function보다 먼저 배치되어야 한다. 추상화 수준이 더 높기 때문이다.
7장. 오류 처리
앞선 3장에서 "오류 코드 리턴보다 try-catch가 낫다"는 내용이 있었다. 이 장에서는 try-catch문의 처리 방법을 다룬다.
- 오류 코드 리턴보다 exception을 사용해야 한다.
- try-catch문을 최대한 뽑아내어 하나로 몰아넣어야 한다.
- 이를 위해 try-catch문은 최대한 작은 범위에서 사용해야 하며, 정상 동작과 오류 동작을 구분해야 한다.
- Unchecked exception을 사용해야 한다. exception을 기술한다면 상위 module이 하위 module에 의존하기 때문이다.
- null을 사용하지 않아야 한다. null 리턴 시 caller function에게 예외처리를 떠넘기며 parameter로 넘기는 경우 exception이 발생하기 때문이다.
8장. 경계
이 장에서는 내부 component와 외부 component 사이의 경계를 다루는 방법을 기술한다. 경계를 잘 처리하면 할수록 변경으로 인한 변경을 줄일 수 있다.
- 경계를 interface로 묶어 관리해야 한다. 코드 변경의 cost가 낮아지기 때문이다.
- 우수한 설계를 통해 코드 변경의 cost를 줄일 수 있다.
- class로 경계를 감싸거나, 또는 adapter 패턴으로 원하는 형식의 interface로 변경한다.
- 경계를 interface로 묶어 미구현된 기능을 구현된 것처럼 포장할 수 있다. 추후 구현된다면 interface의 구현체만 바꿔 끼면 되기 때문에 dependency를 줄일 수 있다.
9장. 단위 테스트
이 장은 프로그램의 무결성을 보장해 주는 Unit Test에 대해 다룬다. Unit Test가 존재하기 때문에 소스코드가 변경되어도 프로그램이 잘 동작함을 생각할 수 있다. 즉 Unit Test는 유연성, 유지보수성을 제공한다.
- TDD는 다음 과정을 거친다.
- 실패하는 Unit Test를 작성할 때까지 실제 코드를 작성하지 않는다
- 컴파일은 성공하되 실행이 실패할 정도로만 Unit Test를 적상한다.
- 실패하는 Unit Test를 통과할 정도로만 실제 코드를 작성한다.
- 테스트 코드도 Clean Code의 규칙을 따라 작성해야 한다. 작성한 테스트 코드도 소스코드의 변경에 따라 변경되어야 하기 때문이다.
- 테스트 코드도 함수이다. 함수는 하나의 역할만 해야 한다. 따라서 테스트 코드는 한 개념만 테스트해야 한다.
- given-when-then template, 그리고 가능한 한 적은 assert를 사용해야 한다.
- FIRST 규칙을 따라야 한다.
- Fast - 빨라야 한다.
- Independent - 테스트 코드는 의존성이 없어야 한다. 의존한다면 테스트 실패 원인을 찾기 어려워지기 때문이다.
- Repeatable - 반복 가능해야 한다.
- Self-validating - boolean 값으로 결과를 내어 쉽게 결과를 확인할 수 있어야 한다.
- Timely - 실 코드를 작성하기 전에 테스트 코드를 작성해야 한다.
12장. 창발성
아래 2개의 단계를 반복함으로써 소스코드의 품질을 높일 수 있다.
- 1. 모든 테스트를 실행한다. - 테스트를 실행하기 위해 작은 class와 method를 만들게 된다.
- 2. 중복을 없앤다. 프로그래머의 의도를 표현한다. class와 method를 최소로 줄인다.
- 테스트 코드를 믿고 이 단계를 반복한다.
13장. 동시성
이 장에서는 동시성을 처리하는 방법을 기술한다. multi-thread 환경은 예상하지 못했던 결과가 나오기 때문에 주의를 기울여야 한다. 추가적으로 부록 A에서 다루는 동시성 II도 정리했다.
- 동시성이 있는 코드는 다른 코드와 분리해야 한다. 동시성 처리만으로 이미 하나의 역할을 하기 때문이다.
- 가능한 thread를 독립적으로 구현해야 한다.
- critical section과 shared resource를 최소한으로 줄여야 한다. lock과 race condition 때문에 부하가 가중되기 때문이다.
- java.util.conturrent 패키지 등 thread-safe한 library를 사용해야 한다.
- thread code를 테스트할 때는 다음과 같은 지침을 지켜야 한다.
- 됐다 안됐다 하는 코드는 틀린 코드다.
- single-thread부터 구성한다.
- 한 번에 많은 thread를 돌려봐야 한다.
- wait(), sleep(), yield() 등의 instrument를 이용해 thread 실행 순서를 바꿔 본다.
- 프로그램 성능 테스트 시 프로세스 연산에 많은 시간을 보낸다면 CPU가 부족한 것이므로 성능을 높여야 한다. I/O 연산에 많은 시간을 보낸다면 동시성 처리가 도움이 된다.
- AtomicBoolean, AtomicInteger, AtomicReference 등 thread-safe한 class를 사용하면 좋다.
- Deadlock이 일어나지 않게 면밀히 검토해야 한다.
17장. Heuristics
Clean Code에서는 많은 code smells와 heuristics를 다룬다. 이 내용들은 위에서 이미 다룬 내용들이기 때문에 중복되지 않고 중요한 몇 가지만 정리하려 한다.
일반
- 중복을 발견한다면 method로 분리하거나 class로 분리해야 한다. switch로 같은 조건을 계속 확인한다면 polymorphism으로 대체해야 한다.
- Open Closed Principle에서 예시를 다룬다.
- 필요 없는 주석, 필요 없는 생성자 필요 없는 모든 것을 깔끔하게 정리해야 한다.
- parameter로 boolean, enum, int code를 넘겨 함수 동작을 제어하는 대신 함수를 쪼개어 해결하는 방법을 강구해야 한다.
- 변하거나 재정의할 가능성이 있는 method는 static으로 작성해서는 안 된다.
- 상수도 naming한 후 저장해야 한다.
- bool 논리를 if문에 사용할 때는 a || b와 같이 풀어 적지 말고 함수로 묶어 작성해야 한다. 가독성이 올라가기 때문이다.
테스트
- 테스트 코드는 모든 경계 조건, 모든 예외를 찾아내고 테스트할 수 있어야 한다.
- 커버리지 도구를 사용해야 한다.
정리
지금까지 Clean Code를 간단하게 정리했다. 처음에 소스 코드는 하나의 글이라 생각한다고 말했다. 결국 Clean Code란 그 이름대로 깔끔한 코드, 읽기 쉬운 글을 작성하는 것과 같다. 이를 위해 명확한 naming, 중복 없애기 등의 방법을 채택하는 것이다.
뭔가를 아는 상태에서, 그리고 필요한 상황에서 읽으니 매우 와 닿는다. 정리한 것들을 참고하며 깔끔한 코드를 작성하기 위해 계속해 고민하고, 개선하고, 리팩토링하자.
'Development > Study' 카테고리의 다른 글
[개발서적] Clean Architecture 정리 (0) | 2023.03.25 |
---|---|
[개발서적] The Pragmatic Programmer 정리 (0) | 2023.03.22 |
[개발서적] Clean Code 정리 (0) | 2022.06.24 |