16 May 2019

도메인 주도 설계 09 - 암시적인 개념을 명확하게

심층적인 모델링이라는 것이 대단하게 느껴지기는 하지만 어떻게 하면 이를 달성할 수 있을까? 심층 모델이 강력한 이유는 모델에 사용자의 행위, 문제, 문제의 해법에 대한 지식을 간결하고 유연하게 표현하는 중심 개념과 추상화가 담겨 있기 때문이다.

심층 모델로 향하는 첫걸음은 일단 도메인의 본질적인 개념을 모델 내에 표현하는 것이다. 그 후 성공적인 지식 탐구와 리팩터링을 반복하면서 이를 정제하게 된다.

경우에 따라서는 과거에 암시적이었던 개념을 명확히하는 것 역시 심층 모델로의 도약에 해당한다. 모델을 정제하면서 반복적으로 리팩터링을 수행하면 모든 것이 또렷해져 명확한 개념으로 발전시킬 수 있다. 그러나 이런 단계는 암시적인 개념을 정제되지 않았다는 것을 인식하는 것에서부터 출발한다.


개념 파헤치기

개발자는 잠재되어 있는 암시적인 개념에 대한 단서에 민감해야 하며, 한발 앞서 미리 암시적인 개념을 찾아야할 때도 있다. 대부분의 암시적인 개념은 팀에서 사용하는 언어를 주의깊게 경청하고, 설계상 부자연스러운 부분과 외견상 모순돼 보이는 전문가의 견해를 면밀하게 검토하며, 도메인과 관련된 문서를 조사하고 수없이 많은 실험 과정을 거쳐 얻어진 것이다.


언어에 귀 기울여라

사용자가 매번 보고서상의 일부 항목에 대해서 반복적으로 이야기 하는 경우가 있다. 하지만 이런 용어의 의미를 이해하지 못할 뿐만 아니라 중요하다는 사실도 미처 깨닫지 못할 때가 있다.

도메인 전문가가 사용하는 언어에 귀를 기울여라. 복잡하게 뒤얽힌 개념을 간결하게 표현하는 용어가 있는가? 여러분이 선택한 단어가 이를 더 적절하게 고쳐주는가? 여러분이 특정 문구를 이야기할 때 도메인 전문가의 얼굴에서 곤혹스러운 표정이 사라지는가? 이 모두가 바로 모델에 기여하는 개념의 실마리에 해당한다.

이를 통해 개발자와 사용자는 서로의 생각을 더 정확하게 이해하게 되고, 특정 시나리오에 대한 모델의 상호작용을 더욱 자연스럽게 표현하는 것이 가능해진다. 도메인 모델 언어는 더욱 강력해지고, 새로운 모델을 반영하기 위해 코드를 리팩터링한 후 설계가 더 깔끔해졌음을 알게 된다.


어색한 부분을 조사하라.

필요한 개념이 늘 대화나 문서로 인식할 수 있을만큼 확연히 드러나 있지 않을 경우가 많다. 이럴 때는 적극적으로 나서서 도메인 전문가가 그러한 개념을 발견할 수 있도록 토론, 지식 탐구를 해야 한다.


모순점에 대해 깊이 고민하라.

천문학자인 갈릴레오는 지동설과 천동설의 모순되는 사실과 주장을 해결하기 위해, 달리는 말 위에 앉아 있는 사람이 공을 떨어뜨렸을 때의 공이 관성으로 이동하는 것을 관찰하고 이를 통해 관성계라는 아이디어의 초기 형태를 이끌어 내어 역설을 해결하고 이를 더 유용한 운동 물리학 모델로 이끌었다.

요구사항을 파헤칠 때면 항상 마주치게 되는 모순점은 더 심층적인 모델에 이르게하는 중요한 단서가 될 수 있다. 이런 모순 사항들을 해결하고 적용하는 방법을 심사숙고하는 과정에서 숨겨져 있던 사실들을 밝히는 계기가 마련될 수 있다.


서적을 참고하라.

모델의 개념을 조사할 때는 분명해 보이는 사실이라고 해서 간과해서는 안된다. 다양한 분야에 대해 근본 개념과 일반적인 통념을 설명하는 책을 살펴봄으로써 좀더 일관성 있고 사려 깊은 관점에서 작업을 시작할 수 있다.

해당 분야의 체계를 잘 이해하고자 하는 문헌을 찾아보거나, 해당 도메인을 경험한 다른 소프트웨어 전문가의 책을 읽는 것도 도움이 된다. 책을 읽는다고 해서 그대로 이용할 수 있는 해법을 얻는 것은 아니지만 해당 분야를 경험한 사람의 정제된 경험을 비롯해 개발자가 시도해볼만한 출발점 정도는 제시해줄 것이다.


명시적인 제약조건

제약조건(constraint)은 특별히 중요한 범주의 모델 개념이다. 흔히 제약 조건은 암시적인 상태로 존재하지만, 이를 명시적으로 표현하면 설계를 대폭 개선시킬 수 있다.

제약 조건이 어떤 객체나 메서드 내에 포함되어 있는 것이 가장 자연스러울 때가 있다. Bucket 객체는 제한된 용량을 초과해서 저장할 수 없다는 불변식을 만족시켜야 한다.

00.png

다음과 같이 메서드 내에 조건 로직을 추가하여 불변식을 보장할 수 있다.

class Bucket {
    private float capacity;
    private float contents;

    public void pourIn(float addedVolume) {
        if (contents + addedVolume > capacity) {
            contents = capacity;
        } else {
            contents = contents + addedVolume;
        }
    }
}

위의 경우에는 로직이 매우 단순해서 규칙을 명확히 식별할 수 있지만, 더 복잡한 클래스 안에 제약조건을 표현할 때는 파악하기가 어려워질 것이다.

다음과 같이 제약 조건을 별도의 메서드로 분리하고 제약조건의 의미를 분명하고 명확하게 표현할 수 있게 메서드의 이름을 짓는다.

class Bucket {
    private float capacity;
    private float contents;

    public void pourIn(float addedVolume) {
      float volumePresent = contents + addedVolume;
      contents = constrainedToCapacity(volumePresent);
    }

    private float constrainedToCapacity(float volumePlacedIn) {
        if (volumePlacedIn > capacity) {
            return capacity;
        }
        return volumePlacedIn;
    }
}

두 번째 예제가 제약과 모델과의 관계를 좀 더 명확히 표현한다. 이처럼 매우 간단한 규칙은 첫 번째 예제처럼 쉽게 이해할 수 있겠지만 규칙이 더 복잡해지면 파악하기가 매우 어려울 것이다.

제약 조건을 별도 메서드로 분리하면 이 제약조건의 의도를 메서드 이름으로 부여할 수 있고, 설계 내에 제약 조건을 명확히 표현할 수 있다. 이를 통해 본래 메서드는 단순한 상태를 유지하고 본연의 작업에만 집중할 수 있다. 또한 제약 조건에 이름을 부여했으므로 이 이름을 갖고 토의도 할 수 있을 것이다.

메서드로 분리하더라도 만족스럽게 제약조건을 표현하지 못할 때도 있다. 특히 객체의 주된 책임을 수행하는데 필요하지 않은 정보를 해당 메서드에서 다룰지도 모른다. 이 것은 규칙이 기존 객체에 존재하기에는 적절치 않은 경우이다.

다음은 어떤 제약 조건을 포함한 객체의 설계가 어딘가 잘못돼 있음을 나타내는 조짐을 나열한 것이다.

  1. 제약 조건을 평가하려면 해당 객체의 정의에 적절하지 않는 데이터가 필요하다.
  2. 관련된 규칙이 여러 클래스에 걸쳐 나타나며, 동일한 계층 구조에 속하지 않는 클래스 간에 중복 또는 상속 관계를 강요한다.
  3. 설계나 요구사항에 대한 논의 때는 제약 조건에 초첨을 맞출 때가 있지만, 정작 구현 단계에서는 절차적인 코드에 묻혀 명시적으로 표현되지 않는다.

제약 조건이 객체가 담당하는 기본 책임을 모호하게 만들거나, 도메인과 관련된 중요한 개념으로 다루어짐에도 불구하고 모델 내에 명확히 표현되지 않는다면 명시적인 객체로 분리하거나 일련의 객체와 관계의 집합으로 모델링해야 된다.


도메인 객체로서의 프로세스

객체지향 패러다임에서 객체는 절차를 캡슐화하여 절차 대신 객체의 목표나 의도에 관해 생각하게 해야 한다.

도메인에 존재하는 어떤 업무 절차를 담당하는 프로세스를 Service로 표현할 때, 복잡한 알고리즘을 한 덩어리로 캡슐화할 때가 많다.

프로세스를 수행하는 방법이 한 가지 이상일 때 취할 수 있는 또 다른 접근법으로는 알고리즘 자체 또는 그것의 일부를 하나의 객체로 만드는 것이다. 어떤 프로세스를 선택할 것인가는 곧 어떤 객체를 선택할 것인가가 되고, 각 객체는 각기 다른 STRATEGY(전략)을 표현한다.

명시적으로 표현해야 할 프로세스와 숨겨야 할 프로세스를 구분하는 것은 바로 도메인 전문가가 이야기하는 프로세스인가 아니면 단순한 프로그램 상의 매커니즘의 일부인가에 달렸다.


제약 조건과 프로세스는 객체지향 언어로 프로그래밍할 때 확연히 떠오르지 않는 넓은 범주의 모델 개념이지만, 일단 이를 모델의 요소로 간주하면 설계를 매우 명확히 만들 수 있다.


SPECIFICATION (명세)

01.png

SPECIFICATION(명세)는 특정한 종류의 규칙을 표현하는 매우 간결한 수단을 제공하며, 조건 로직으로부터 규칙을 분리하여 규칙이 모델 내에서 분명해지게끔 만들어준다.

보통 객체에는 해당 객체의 규칙을 검사하는, 다음과 같이 Boolean 값을 리턴하는 테스트 메서드가 있다.

public boolean isOverdue() {
    Date currentDate = new Date();
    return currentDate.after(dueDate);
}

그런데 이런 규칙이 위의 코드처럼 단순한게 아니고, 요구사항에 따라서는 아주 복합한 구현을 필요로 할 수 있다. 이럴 경우, 해당 규칙에 대한 구현 코드가 해당 객체의 책임을 넘어설 수가 있다. 아니면 객체와는 상관없는 상태를 통해 규칙을 구현해야 하는 경우가 있을 수 있다.

이럴 때는 해당 규칙의 구현을 리팩터링하여 도메인 객체 밖으로 빼내겠지만, 이러면 한 업무 규칙을 표현하는 코드가 도메인 계층의 밖으로 분리되어버린다. 규칙 구현 코드를 도메인 계층 내에 유지할 필요는 있지만 규칙을 통해 평가하려는 객체에 코드를 두기에는 적절치 않는 상황인 것이다.

종종 업무 규칙이 Entity나 Value Object가 맡고 있는 책임에 맞지 않고, 규칙의 다양성과 조합이 도매인 객체의 본래 의미를 압도할 때가 있다. 그렇다고 규칙을 도메인 계층으로부터 분리한다면 도메인 코드가 더는 모델을 표현할 수 없게 되므로 상황은 더욱 악화될 것이다.

이를 위해 특정 객체에 대한 평가 결과를 Boolean 값으로 결과를 리턴하는 특별한 객체를 만들 수 있다. 이러한 객체는 어느 특정한 도메인 객체에 대하여 평가를 할 수 있는 참 / 거짓 테스트를 수행하는 Value Object 이다.

02.png

위의 그림과 같이 Invoice 객체의 특정 상태를 검사하는 책임을 InvoiceDelinquency 객체로 옮기는 것이다. 이러한 객체를 Specification(명세)이라고 하며, 다른 객체에 대한 제약 조건을 기술한다. Specification은 다른 객체가 Specification에 명시된 기준을 만족하는지 검사하는 것이다.

이 Specification을 통해 어느 도메인 객체에 대한 규칙을 도메인 계층에 유지시킬 수 있다. 아울러 완전한 객체를 통해 규칙을 표현하므로 설계가 모델을 더욱 명확히 반영할 수 있다.


Specification의 적용과 구현

Specification의 주된 가치는 매우 상이해보이는 애플리케이션의 기능을 하나로 통합해준다는 점이다. 객체의 상태와 관련해서 다음 세 가지의 기능을 하나의 객체로 명시할 수 있다.

  1. 검증: 객체가 어떤 요건을 충족시키거나 특정 목적으로 사용될 수 있는지 객체를 검증
  2. 선택: Repository나 어느 컬렉션 내에서 객체를 선택하거나 조회
  3. 요청 구축: Specification에 명시된 규칙, 요구사항을 만족하는 새로운 객체의 생성

도메인 객체의 특정 상태를 검사할 때나, 데이터베이스에서 특정 상태를 만족하는 객체를 가져온다는지, 어느 조건을 만족하는 객체를 생성시 Specification을 사용할 수 있다.


Tags:
Stats:
0 comments