실제로 어떻게 달성할 수 있을까?
심층 모델이 강력한 이유는 심층 모델에 사용자의 행위, 문제, 문제의 해법에 대한 본질적인 지식을 간결하고 유연하게 표현하는 중심 개념과 추상화가 담겨 있기 때문이다. 심층 모델에 향하는 첫걸음은 일단 도메인의 본질적인 개념을 모델 내에 표현하는 것이다. 그 후 성공적인 지식탐구와 리팩터링을 반복하면서 이를 정제하게 된다. 그러나 지식탐구와 리팩터링은 중요한 개념이 모델과 설계 내에 명확하게 인식되고 표현될 때에야 비소로 본 궤도에 오른다.
발견된 대부분의 암시적인 개념은 팀에서 사용하는 언어를 주의 깊게 경청하고, 설계상 부자연스러운 부분과 외견상 모순돼 보이는 전문가의 견해를 면밀하게 검토하며, 도메인과 관련된 문서를 조사하고 수없이 많은 실험 과정을 거쳐 얻어진 것이다.
하지만 이것이 “명사는 객체다”라는 진부한 개념을 표현하는 것이 아니다. 새로운 단어를 듣게 되면 명료하고 유용한 개념을 찾기 위한 대화와 지식탐구로 이어진다.
사용자나 도메인 전문가가 설계상의 어디에도 표현돼 있지 않은 어휘를 사용하고 있다면 그것은 곧 경고 신호다.
설계상에 표현돼 있지 않은 어휘를 사용하고 있다면 그것은 더욱 더 위험한 경고다.
모델의 개념을 조사할 때는 분명해 보이는 사실이라고 해서 대강 보고 넘겨서는 안된다. 다양한 분야에 대해 근본 개념과 일반적인 통념을 설명하는 책을 찾아볼 수 있다.
…
다른 대안으로는 해당 도메인을 경험한 다른 소프트웨어 전문가의 책을 읽는 것이다.
예를 들어 “Analysis Patterns: Reusable Object Models (Fowler 1997)“의 6장을 읽었다면 좋든 나쁘든 간에 앞의 결과와는 전혀 다른 방향으로 작업을 진행했을 것이다. 책을 읽는다고 해서 그대로 이용할 수 있는 해법을 얻는 건 아니다. 다만 해당 분야를 두루 경험한 사람의 정제된 경험을 비롯해 개발자가 직접 시도해볼 만한 출발점 정도는 제시할 것이다.
덕분에 개발자는 바퀴를 다시 발명하는 수고를 아낄 수 있다.
11장, “분석 패턴의 적용”에서는 이와 같은 방식을 좀 더 깊이 있게 다루겠다.
어차피 선택의 여지는 없다. 실험은 유용한 것이 무엇이고 유용하지 않은 것이 무엇인지를 배우는 방법이다.
Constraint는 특별히 중요한 범주의 모델 개념을 형성한다. 흔히 제약조건은 암시적인 상태로 존재하며, 이를 명시적으로 표현하면 설곌ㄹ 대폭 개선할 수 있다.
제약 조건을 별도의 메서드로 분리하면 부여된 이름을 사요하여 제약조건에 관한 토의가 가능해지고, 호출 메서드는 단순한 상태를 유지하고 본연의 작업에만 집중할 수 있다.
다음은 어떤 제약조건을 포함한 객체의 설계가 어딘가 잘못돼 있음을 나타내는 조짐을 일부 나열한 것이다.
프로세스를 수행하는 방법이 한 가지 이상일 때 취할 수 있는 또 다른 접근법은 알고리즘 자체 또는 그것의 일부를 하나의 객체로 만드는 것이다. → Strategy pattern
도메인 전문가가 이야기하고 있는 프로세스인가?
아니면 컴퓨터 프로그램상의 메커니즘의 일부일 뿐인가?
☞ SPECIFICATION : 특정한 종류의 규칙을 표현하는 매우 간결한 수단을 제공하며, 조건 로직으로부터 규칙을 분리해서 규칙이 모델 내에서 분명해지게끔 만들어준다.
규칙을 도메인 계층 내에 유지할 필요가 있지만 규칙을 통해 평가하려는 객체에 규칙을 두기에는 적절하지 않다. 그뿐만 아니라 규칙을 평가하는 메서드는 조건 코드로 팽창할 것이고 결국 규칙에 대한 가독성은 떨어지고 만다.
예시 : JPA의 Specification, Java의 Predicate
객체의 상태를 다음과 같은 세 가지 목적으로 명시하고 싶을 때가 있다.
1. 객체가 어떤 요건을 충족시키거나
특정 목적으로 사용할 수 있는지
가늠하고자 객체를 검증 → validation 2. 컬렉션 내의 객체를 선택(이를테면, 기한이 만료된 송장 목록을 조회) → selection 3. 특정한 요구사항을 만족하는 새로운 객체의 생성을 명시 → building to order (요청 구축)
// Specificaiton에 맞는 컬렉션 내의 객체만을 선택한다. Set<Product> products = productRepository.selectSatisfying(new RatioSpecification(value, dueDate));
public String asSQL(value, dueDate) {
return "SELECT * FROM PRODUCT" +
... + SQLUtility.productAsSQL(value, dueDate);
}
Specificaiton을 사용해서 객체 생성기의 인터페이스를 정의하면 생성할 결과물을 명시적으로 인터페이스에 포함킬 수 있다. 이 접근법에는 여러 가지 이점이 있다.
@RequiredArgsConstructor
public class ContainerSpecification {
private final ContainerSpecification requiredSpecification;
public boolean isSatisfiedBy(Container container) {
return container.getFeatures().contains(requiredSpecification);
}
}
@Getter
@RequiredArgsConstructor
public class Container {
private final int capacity;
private final Collection<Drum> contents;
private final Collection<ContainerFeature> features;
private boolean hasSpaceFor(Drum drum) {
return remainingSpace() >= drum.getSize();
}
private double remainingSpace() {
double totalContentsSize = 0;
for (Drum content : contents) {
totalContentsSize += content.getSize();
}
return capacity - totalContentsSize;
}
public boolean canAccommodate(Drum drum) {
return hasSpaceFor(drum) &&
drum.getChemical().getContainerSpecification().isSatisfiedBy(this);
}
public void add(Drum drum) {
contents.add(drum);
}
}
public class PrototypePacker implements WarehousePacker {
@Override
public void pack(Collection<Container> containersToFill, Collection<Drum> drumsToPack) throws NoAnswerException {
for (Drum drum : drumsToPack) {
Container container = findContainerFor(containersToFill, drum);
container.add(drum);
}
}
private Container findContainerFor(Collection<Container> containers, Drum drum) {
for (Container container : containers) {
if (container.canAccommodate(drum)) {
return container;
}
}
throw new NoAnswerException();
}
}