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

Hexagonal Architecture - Mapping

웹, 애플리케이션, 도메인, 영속성 각 계층의 모델을 매핑하는 전략들을 정리하자.

  • 매핑하지 않기
  • 양방향 매핑
  • 완전 매핑
  • 단방향 매핑

매핑하지 않기

포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면, 계층 간 매핑이 필요없다.

하지만, 웹 계층과 영속성 계층의 모델에서는 특별한 요구사항이 있을 수 있다.
웹 계층에서는 JSON 으로 직렬화하기 위한 annotation 을 모델 클래스의 특정 필드에 붙여야할 수 있다.
영속성 계층에서는, DB 매핑을 위해 특정 annotation 이 필요할 수 있다.

또한, Account 클래스는 각 계층과 관련된 이유로 변경되어야해서 단일 책임 원칙을 위반한다.
각 계층이 Account 클래스에 특정 커스텀 필드를 두도록 요구할 수도 있다.

양방향 매핑

각 어뎁터가 전용 모델을 가지고 있다.
그래서, 해당 모델을 도메인 모델로, 도메인 모델을 해당 모델로 매핑할 책임이 있다.
안쪽 계층은 해당 계층의 모델만 알면되고 도메인 로직에만 집중한다.

Read more