도서명 | 쪽수 | 저자, 출판사 | 발행일 | |||||
![]() |
Clean Code : 클린 코드 |
584 | 로버트 C. 마틴/박재호, 인사이트(insight) |
2013년 12월 24일 |
들어가면서
이 책에 나오는 모든 지침은 로버트 C. 마틴이 이미 밝혔듯이 절대적이라 생각하면 안 되며, 언제든지 개선의 여지가 있다고 생각하는 편이 바람직하다. 여기서 핵심은 팀이나 공동체에서 서로 동의하는 합리적인 원칙을 세우기 위한 소통에 있다.
이 책은 세 부분으로 나눠진다.
첫번째 - 깨끗한 코드를 작성하는 원칙, 패턴, 실기
두번째 - 여러 사례 연구 소개. 코드를 깨끗하게 고치는, 즉 문젝 있는 코드를 문제가 더 적은 코드로 바꾸는 연습
세번째 - 사례 연구를 만들면서 수집한 냄새와 휴리스틱을 마지막 장에서 열거한다.
여러 사례 연구를 검토하고, 모든 결정과 단계를 이해하고, 우리 입장에 서서 우리가 생각한 방식을 이해하려 애쓴다면, 이 책에서 제시하는 원칙과 패턴과 실기와 휴리스틱을 자신의 지식으로 만들 수 있으리라 생각한다.
1. 깨끗한 코드
- 깨끗한 코드는 단순하고 직접적이며, 잘 쓴 문장처럼 읽혀야 한다.
- 작성자가 아닌 사람도 읽기 쉽고 고치기 쉬워야 하며, 단위 테스트 케이스와 인수 테스트 케이스가 존재한다.
- 주의 깊게 작성한 코드이다. 즉, 시간을 들여 깔끔하고 단정하게 정리한 코드이다.
- 중복 줄이기, 표현력 높이기, 작게 추상화하기
- 철저한 오류 처리, 메모리 누수, 경쟁 상태, 일관성 없는 명명법 등 세세한 사항까지 꼼꼼하게 신경 써야 한다.
2. 의미 있는 이름
- 의도를 분명히 밝혀라
- 그릇된 정보를 피하라
- 의미 있게 구분하라
- 발음하기 쉬운 이름을 사용하라
- 검색하기 쉬운 이름을 사용하라
- 인코딩을 피하라 (이제는 헝가리식 표기법 X, 멤버 변수 접두어 X)
- 자신의 기억력을 자랑하지 마라
- 클래스 이름은 명사나 명사구가 적합하다.
- 메서드 이름은 동사나 동사구가 적합하다.
- 기발한 이름은 피하라
- 한 개념에 한 단어를 사용하라
- 말장난을 하지 마라
- 해법 영역에서 가져온 이름을 사용하라
- 문제 영역에서 가져온 이름을 사용하라
- 의미 있는 맥락을 추가하라
- 불필요한 맥락을 없애라
3. 함수
- 함수를 만드는 첫째 규칙 - 작게 만들어라
- if, else, while 문 등에 들어가는 블록은 한 줄이어야 한다.
- 한 가지만 해라
- 함수 당 추상화 수준은 하나로
- 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.(내려가기 규칙)
- 서술적인 이름을 사용하라
- 함수에서 이상적인 인수 개수는 0개이다. 4개 이상은 사용하지 않는다.
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
- 플래그 인수는 추하다. 왜냐면 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이기 때문
- 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
- 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 신중히 고려해야 한다.
- 부수 효과를 일으키지 마라
- 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. ex) void appendFooter(StringBuffer report)
- 명령과 조회를 분리하라
- 오류코드보다 예외를 사용하라
- 코드를 반복하지 마라
- 구조적 프로그래밍 - goto 문은 큰 함수에서만 의미가 있으므로, 작은 함수에서는 피해야만 한다.
- 대가(master) 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다.
4. 주석
- 우리에게 프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면, 주석은 전혀 필요하지 않다.
- 우리는 코드로 의도를 표현하지 못해, 그러니까 실패를 만회하기 위해 주석을 사용한다.
- 주석은 오래될수록 코드에서 멀어지고, 완전히 그릇될 가능성도 커진다.
- 부정확한 주석은 아예 없는 주석보다 훨씬 더 나쁘다.
- 주석은 나쁜 코드를 보완하지 못한다.
- 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다. 자신이 저지른 난장판을 주석으로 설명하려 애쓰는 대신에 그 난장판을 깨끗ㅇ이 치우는 데 시간을 보내라! (ㅠ,.ㅠ)
- 코드로 의도를 표현하라
- 좋은 주석
- 법적인 주석 : 소스 파일 첫머리에 주석으로 들어가는 회사의 저작권 정보와 소유권 정보 등
- 정보를 제공하는 주석 : 때로는 기본적인 정보를 주석으로 제공하면 편리하다.
- 의도를 설명하는 주석
- 의미를 명료하게 밝히는 주석
- 결과를 경고하는 주석
- TODO 주석
- 중요성을 강조하는 주석
- 나쁜 주석
- 주절거리는 주석
- 같은 이야기를 중복하는 주석
- 오해할 여지가 있는 주석
- 의무적으로 다는 주석
- 이력을 기록하는 주석 (이전에는 소스 코드 관리 시스템이 없었기 때문에 이런 관례가 있었다.)
- 있으나 마나 한 주석
- 위치를 표현하는 주석 ex) // Axtions ////////////////////////////////
- 닫는 괄호에 다는 주석 (닫는 괄호에 주석을 달아야겠다는 생각이 든다면 대신에 함수를 줄이려 시도하자)
- 공로를 돌리거나 저자를 표시하는 주석 (소스 코드 관리 시스템에 저장하자)
- 주석으로 처리한 코드 (그냥 삭제해라)
- HTML 주석
- 전역 정보
- 너무 많은 정보 (흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓지 마라)
- 모호한 관계 (주석 자체가 다시 설명을 요구하는 경우)
- 함수 헤더
- 비공개 코드에서 Javadocs (공개 API는 Javadocs가 유용하지만, 비공개 코드는 쓸모 없다.)
5. 형식 맞추기
- 형식을 맞추는 목적
- 코드 형식은 의사소통의 일환이다.
- 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다. 오랜 시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.
- 적절한 행 길이를 유지하라
- 신문 기사처럼 작성하라
- 개념은 빈 행으로 분리하라
- 세로 밀집도 (서로 밀접한 코드 행은 세로로 가까이 놓여야 한다)
- 수직 거리 (같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다. 가까이)
- 변수 선언 - 사용하는 위치에 최대한 가까이 선언
- 인스턴스 변수 - 클래스 맨 처음에 선언
- 종속 함수 - 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 호출하는 함수 -> 호출되는 함수
- 개념적 유사성 (비슷한 동작을 수행하는 일군의 함수)
- 세로 순서 - 가장 중요한 개념을 가장 먼저 표현
- 가로 형식 맞추기
- 짧은 행이 바람직하다. (80자 이하 정도, 최대 120자)
- 가로 공백과 밀집도
private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); }
- 아래와 같은 가로 정렬을 하지 않는다.
private Socket socket;
private InputStream input;
private OutputStream ouput; - 들여쓰기
- 파일 수준인 문장은 뜰여쓰지 않는다.
- 클래스 내 메서드는 클래스보다 한 수준 들여쓴다.
- 메서드 코드는 메서드 선언보다 한 수준 들여쓴다.
- 블록 코드는 블록을 포함하는 코드보다 한 수준 들여쓴다.
- 팀 규칙
- 프로그래머라면 각자 선호하는 규칙이 있다. 하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.
- 팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 소프트웨어가 일관적인 스타일을 보인다. (어디에 괄호를 넣을지, 들여쓰기는 몇자로 할지, 클래스와 변수와 메서드 이름은 어떻게 지을지 등 결정)
- 밥 아저씨의 형식 규칙 (코드 자체가 최고의 구현 표준 문서가 되는 예이다)
public class CodeAnalyzer implements JavaFileAnalysis {
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount() {
return lineCount;
}
public int getMaxLineWidth() {
return maxLineWidth;
}
public int getWidestLineNumber() {
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}
public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}
public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}
private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
6. 객체와 자료 구조
변수를 비공개 private 로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. 충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서다.
그렇다면 어째서 수많은 프로그래머가 조회 get 함수와 설정 set 함수를 당연하게 공개 public해 비공개 변수를 외부에 노출할까?
- 자료 추상화
- 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.
- 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.
- 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다
public interface Vehicle { double getPercentFuelRemaining(); }
- 자료/객체 비대칭
- (자료 구조를 사용하는) 절차적인 코드
- 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
- 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.
- 객체 지향 코드
- 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
- 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
- (자료 구조를 사용하는) 절차적인 코드
- 디미터 법칙
- 디미터 법칙은 잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
- 기차 충돌
- final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath() 와 같은 코드를 기차 충돌 이라고 부른다. 일반적으로 조잡하다 여겨지는 방식이므로 피하는 편이 좋다.
- 자료구조 일 경우 다음과 같이 표현
-> final String outputDir = ctxt.options.scratchDir.absolutePath;
- 객체 일 경우 뭔가를 하라고 지시
-> BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
- final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath() 와 같은 코드를 기차 충돌 이라고 부른다. 일반적으로 조잡하다 여겨지는 방식이므로 피하는 편이 좋다.
- 잡종 구조
- 절반은 객체, 절반은 자료 구조인 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다. 되도록 피하는 편이 좋다. 프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해(더 나쁘게는 무지해) 어중간하게 내놓은 설계에 불과하다.
- 자료 전달 객체 (DTO - Data Transfer Object)
- 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스이고, 일반적인 형태는 '빈 bean' 구조이다.
- 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용하게 사용
- 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체이다.
public class Address { private String street; private String streetExtra; public Address(String street, String streetExtra) { this.street = street; this.streetExtra = streetExtra; } public String getStreet() { return street; } public String getStreetExtra() { return streetExtra; } }
- 활성 레코드
- DTO의 특수한 형태. 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과이다. 자료구조로 취급해야 한다.
- 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save나 find와 같은 탐색 함수도 제공한다.
- 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하다. 이런 잡종 구조는 바람직 하지 않다.
- (어떤) 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
우수한 소프트웨어 개발자는 편견없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.
7. 오류 처리
오류 처리는 중요하다. 하지만 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.
- 오류 코드보다 예외를 사용하라
- 논리가 오류 처리 코드와 뒤섞이지 않도록, 오류가 발생하면 예외를 던지는 편이 더 깔끔하다.
- Try-Catch-Finally 문장부터 작성하라
- 예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.
- 미확인 unchecked 예외를 사용하라
- 예외에 의미를 제공하라
- 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.
- 호출자를 고려해 예외 클래스를 정의하라
- 아래 예시는 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환한다. (감싸기 wrapper 클래스)
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
- 정상 흐름을 정의하라
- null을 반환하지 마라
- 아래와 같은 코드처럼 null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
// 아래와 같이 코드를 변경하여 NullPointerException이 발생 할 가능성을 줄이자
public List<Employee> getEmployees() {
if ( ..직원이 없다면 ..) {
return Collections.emptyList();
}
}
- 메서드 인수에 null을 전달하지 마라
8. 경계
소프트웨어 경계를 깔끔하게 처리하는 기법과 기교
- 외부 코드 사용하기
- Map을(혹은 유사한 경계 인터페이스) 여기저기 넘기지 말고, 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.
// Sensor라는 객체를 담는 Map을 만들고 가져올때 아래와 같이 작성할 경우 코드 의도가 분명히 드러나지 않는다.
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);
// 아래처럼 더 깔끔하게 사용해보자. Sensors 사용자는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다.
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor)sensors.get(id);
}
// 이하 생략
}
- 경계 살피고 익히기
- 타사 라이브러리를 가져다 사용할때에는, 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자. 이를 '학습 테스트'라고 부른다.
- 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. (API를 사용하려는 목적에 초점을 맞춘다.)
- log4j 익히기 (책에 적혀있는거보다 더 최근에 나온 log4j2 를 익히자)
- 학습 테스트는 공짜 이상이다 - 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다.
학습 테스트란? 자신이 만들지 않은 프레임워크나 라이브러리 등에 대해 작성하는 테스트이다. 목적 = 사용 방법을 익히고 검증 - 아직 존재하지 않는 코드를 사용하기
- 깨끗한 경계
- 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map에서 봤듯이 새로운 클래스로 경계를 감싸거나,
ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.
- 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map에서 봤듯이 새로운 클래스로 경계를 감싸거나,
9. 단위 테스트
- TDD 법칙 세 가지
- 1) 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 2) 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 3) 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
- 깨끗한 테스트 코드 유지하기
- 테스트 코드는 실제 코드 못지 않게 깨끗하게 짜야 한다. (중요)
- 깨끗하게 짜기 위해서는 가독성이 제일 중요하다.
- 명료성, 단순성, 풍부한 표현력이 필요하지만, 실제 코드만큼 효율적일 필요는 없다.
- 테스트 함수 당 assert 하나만 사용하는게 좋지만, 경우에 따라 여러개를 넣기도 한다.
- 테스트 함수 당 개념 하나만 테스트하라
- F.I.R.S.T.
- 빠르게 Fast : 테스트는 빨리 돌아야 한다.
- 독립적으로 Independent : 각 테스트는 서로 의존하면 안된다. 어떤 순서로 실행해도 괜찮아야 한다.
- 반복가능하게 Repeatable : 어떤 환경에서도 반복 가능해야 한다. (실제 환경, QA 환경, 버스 타고 집 가는 노트북 환경)
- 자가검증하는 Self-Validating : 테스트는 부울 bool 값으로 결과를 내야 한다. (성공 아니면 실패)
- 적시에 Timely : 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
10. 클래스
- 클래스 체계
- 가장 먼저 변수 목록이 나온다. 정적 static 공개 public 상수가 있다면 맨 처음에 나온다. 다음으로는 정적 비공개 private 변수가 나오며, 이어서 비공개 인스턴스 변수가 나온다.
변수 목록 -> 공개 함수 -> 비공개 함수 (자신을 호출하는 공개 함수 직후)
- 가장 먼저 변수 목록이 나온다. 정적 static 공개 public 상수가 있다면 맨 처음에 나온다. 다음으로는 정적 비공개 private 변수가 나오며, 이어서 비공개 인스턴스 변수가 나온다.
- 클래스는 작아야 한다
- 클래스 이름은 해당 클래스 책임을 기술해야 한다.
- 단일 책임 원칙 Single Responsibility Principle (클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙)
- 큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
- 응집도 - 클래스는 인스턴스 변수가 작아야 한다.
- 큰 함수 일부를 작은 함수 하나로 빼내고 싶은데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다. 그렇다면 변수 네 개를 새 함수에 인수로 넘겨야 옳을까?
-> 전혀 아니다, 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리해서 쪼개라
- 큰 함수 일부를 작은 함수 하나로 빼내고 싶은데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다. 그렇다면 변수 네 개를 새 함수에 인수로 넘겨야 옳을까?
- 변경으로부터 격리 - 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.
11. 시스템
- 시스템 제작과 시스템 사용을 분리하라
- Main 분리 -> 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.
- 팩토리 -> 물론 때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다. 이때는 ABSTRACT FACTORY 패턴을 사용한다
12. 창발성
13. 동시성
14. 점진적인 개선
15. JUnit 들여다보기
16. SerialDate 리팩터링
17. 냄새와 휴리스틱
부록 A. 동시성 II
부록 B. org.jfree.date.SerialDate
부록 C. 휴리스틱의 교차 참조 목록