Emergence

켄트 백은 다음 규칙을 따르는 설계를 “단순하다”고 말한다.

  1. 모든 테스트를 실행한다.
  2. 중복을 없앤다.
  3. 프로그래머 의도를 표현한다.
  4. 클래스와 메서드 수를 최소로 줄인다.

모든 테스트를 실행해라

테스트 케이스를 만들고 계속 돌리자.
그러면, 시스템은 낮은 결합도와 높은 응집력을 따르게 된다.
테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 높아진다.

크키가 작고 목적 하나만 수행하는 클래스가 나오게 된다.
SRP 를 준수하면 테스트가 쉬워지기 때문이다.

결합도가 높으면 테스트 케이스 작성이 어렵다.
그래서, DIP 원칙을 적용하고 DI, 인터페이스, 추상화 같은 도구를 사용해 결합도를 낮추게 된다.

리팩토링을 해라

테스트 케이스를 모두 작성했으면, 리팩토링을 하자.
테스트 케이스가 있으므로, 코드를 정리하면서 시스템이 깨질 걱정할 필요가 없다.

중복을 없애라

깨끗한 시스템을 만들려면 단 몇줄이라도 중복을 제거해야한다.
중복은 추가 위험, 불필요한 복잡도를 뜻한다.

Read more

Classes

깨끗한 클래스를 만드는 방법을 정리한다.

클래스는 작아야한다

1
2
3
4
5
6
7
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}

위 SuperDashboard 는 책임이 너무 많다.

클래스 이름은 클래스 책임을 기술해야하는데, 클래스 이름이 모호하면 책임이 많아서이다.
예를 들면, Processor, Manager, Super

그리고 클래스 설명은 if, and, or, but 을 사용하지 않고 가능해야한다.
SuperDashboard 클래스는 다음 처럼 설명할 수 있다:
“마지막으로 포커스를 얻은 컴포넌트에 접근하는 방법을 제공하며, 버젼과 빌드 번호를 추적하는 메커니즘을 제공한다”

“~하며” 는 SuperDashboard 에 책임이 많다는 증거다.

단일 책임 원칙

SRP 는 클래스나 모듈을 변경할 이유 (책임) 가 단 하나뿐이어야한다는 원칙이다.
SuperDashboard 는 변경할 이유가 두 가지다.

  1. 소프트웨어 버젼 정보를 추적하는데, 소프트웨어를 출시할 때마다 달라진다.
  2. 스윙 컴포넌트를 관리하는데, 스윙 코드가 변경되면 버젼 번호가 달라진다.
Read more

Unit Tests

테스트 코드가 방치되어 망가지면, 실제 코드도 망가진다.
테스트 코드를 깨끗하게 유지하는 방벙을 정리하자.

TDD 법칙 세 가지

  1. 실패하는 단위 테스트를 작성하기 전에, 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로 실제 코드를 작성한다.

위와 같이 하면, 실제 코드를 전부 테스트하는 테스트 케이스가 나온다.
하지만 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.

깨끗한 테스트 코드 유지하기

테스트 코드는 실제 코드 못지 않게 중요하다.
테스트 코드가 복잡할 수록,

  1. 실제 코드를 작성하는 시간보다, 테스트 케이스를 추가하는 시간이 더 걸린다.
  2. 실제 코드를 변경해 기존 테스트 케이스가 실패하면, 실패 테스트 케이스를 더 통과하기 어려워 진다.
  3. 팀이 테스트 케이스를 유지 보수하는 비용도 늘어나게 된다.

하지만 테스트 슈트가 없으면 수정한 코드가 제대로 동작하는지 확인할 방법이 없다.
테스트 케이스가 있으면 변경이 두렵지 않다.
테스트 케이스가 없다면 모든 변경이 잠적정인 버그이다.

깨끗한 테스트 코드

깨끗한 테스트 코드를 만들려면, 가독성이 중요햐다.
가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다.

Read more

Boundaries

외부 코드를 우리 코드에 깔끔하게 통합하는 방법을 정리한다.

외부 코드 사용

java.util.Map 은 아래처럼, 다양한 인터페이스를 제공한다.

https://docs.oracle.com/javase/8/docs/api/java/util/Map.html

Map 이 제공하는 기능성과 유연성은 유용하지만, 위험도 크다.
Map 을 만들어서 여기저기 넘긴다고 하자.
넘기는 쪽에서는 Map 내용을 삭제하지 않는다고 생각할 수 있다.
하지만, Map 사용자라면 clear() 로 내용을 지울 권한이 있다.
또한, Map 은 객체 유형을 제한하지 않기 때문에, 누구나 어떤 객체 유형도 추가할 수 있다.

이런 위험을 개선하는 코드를 보자.

1
2
3
4
5
6
7
public class Sensors {
private Map sensors = new HashMap();

public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
}

경계 인터페이스인 Map 을 Sensors 안으로 숨겼다.
그래서, Map 인터페이스가 변해도 프로그램에 영향을 미치지 않는다.
그리고, Sensors 클래스는 필요한 인터페이스만 제공해서 코드를 이해하기 쉽고 오용하기 어렵다.
Sensors 클래스는 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.

경계 살피고 익히기

외부에서 가져온 패키지를 사용하고 싶으면 학습 테스트부터 시작하자.
우리 코드를 작성해 외부 코드를 호출하는 대신, 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자.
학습 테스트는, API 를 사용하려는 목적에 초점을 맞춘다.

Read more

Error Handling

오류 처리 코드로 프로그램 논리를 이해하기 어려워지면, 깨끗한 코드가 아니다.
오류를 처리하는 기법과 고려 사항을 정리하자.

오류 코드 보다 예외를 사용하자

오류 코드를 반환하는 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
public class DeviceController {

public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
if (handle != DeviceHandle.INVALID) {
DeviceRecord record = retrieveDeviceRecord(handle);
if (record.getStatus() != DEVICE_SUSPENDED) {
...
}
}
}
}

위 코드는, 호출자 코드가 복잡하다.
함수를 호출한 즉시, 오류를 확인해야하기 때문이다.

아래 처럼, 예외를 던지자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DeviceController {

public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}

private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);

pauseDevice(handle);
...
}
}

Try-Catch-Finally 문부터 작성하자

try 블록은 트랜잭션과 비슷하다.
try 블록에서 무슨 일이 생기든, catch 블록은 프로그램 상태를 일광성 있게 유지해야한다.

예외가 발생할 코드를 짤 때는, try-catch-finally 문으로 시작하자.
그러면, try 블록에서 무슨 일이 생기든 호출자가 기대하는 상태를 정의하기 쉽다.

Read more

Objects and Data Structures

객체와 자료구조의 차이를 정리하자.

자료 추상화

자료를 세세하게 공게하기 보다, 추상적인 개념으로 표현하자.
아무 생각 없이 조회/설정 함수를 추가하지 말자.

2차원 점을 표현하는 아래 두 코드의 차이를 보자.

1
2
3
4
public class Point {
public double x;
public double y;
}

위 코드는 구현을 외부에 노출한다.
변수를 private 으로 선언하더라도, 각 값마다 get, set 함수를 제공하면 구현을 외부로 노출하는 셈이다.
확실히 직교좌표계를 사용하고, 개별적으로 좌표값을 읽고 설정하게 강제한다.

1
2
3
4
5
6
7
8
public interface Point {
doube getX();
double getY();
void setCartesian(double x, doube y);
double getR();
double getTheta();
void setPolar(double r, doube theta);
}

위 코드는 구현을 숨긴다.
직교좌표계를 사용하는지 극좌표계를 사용하는지 알 수 없다.
그리고 자료구조 이상을 표현한다. 클래스 메서드가 접근 정책을 강제한다. 좌표를 읽을 때는 개별적으로 읽고, 설정할 때는 두 값을 한꺼번에 설정해야한다.

아래 두 코드를 보자.

1
2
3
4
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
Read more

Formatting

코드 형식을 맞추는 방법을 정리하자.

적절한 행 길이를 유지하자

큰 파일보다 작은 파일이 이해하기 쉽다.

신문 기사처럼 작성하자

이름은 간단하고 설명이 가능하게 짓는다. 이름만 보도 올바른 모듈인지 판단가능하도록.

소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명하고, 아래로 내려갈수록 의도를 자세하게 묘사하자.
마지막에는 가장 저차원 함수와 세부 내용이 나온다.

개념은 빈 행으로 분리하자

패키지 선언부, import 문, 각 함수 사이에 빈 행이 들어간다.
빈 행은 새로운 개념이 시작한다는 단서다.

서로 밀접한 코드 행은 세로로 가까이 놓자

줄바꿈이 개념을 분리한다면, 세로 밀집도는 연관성을 의미한다.
연관성이란, 한 개념을 이해하기 위해 다른 개념이 중요한 정도다.
연관성 깊은 두 개념이 서로 떨어져 있으면, 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다.

Read more