28 Apr 2019

도메인 주도 설계 05 - 소프트웨어에서 표현되는 모델

Model Driven Design의 효과를 놓치지 않으면서 구현과 조화를 이루려면, 모델과 구현은 상세 수준에서 연결되어야 한다는 점이다.

도메인 개념을 담은 객체를 정의하는 일은 겉으로는 매우 쉬워보이지만 의미 상의 미묘한 차이로 발생할 수 있는 중대한 문제가 생길 수 있다. 각 모델 요소의 의미를 명확히 하고, 특정 종류의 객체를 도출하기 위한 일정한 구분법이 있다.

  • 어떤 객체가 연속성과 식별성을 지니고 있는가? -> Entity
  • 상태를 기술하는 속성에 불과한가? -> Value Object
  • 상태보다는 행동이나 연산으로 명확히 표현되는 것 -> Service
    • 상태를 주고받지는 않는 행위를 모델링하는 경우

이런 기본 요소들은 관례적인 것으로, 그 개념을 따르는 모델링과 설계 경향에 대해 책이나 문헌으로 존재하였다. 이런 개념에 맞도록 더 규모가 큰 모델과 설계 문제를 다룰 때, 개발자들이 도메인 주도 설계의 우선순위에 부합하는 세부 구성요소를 만드는데 도움이 될 수 있다.


연관관계

모델링과 실제 구현 간의 상호작용은 여러 객체 간의 연관관계에서 특히 까다롭다.

서로 다른 모델이나 객체끼리는 연관관계가 있다. 어느 두 사물들끼리의 연관관계를 나타내는 것은 두 사물에 관계된 것이다. 이는 개발자가 실제 사물들끼리의 관계를 추상화한 것이기도 하다. 한편 구현 관점에서는 두 객체 간의 참조이거나 데이터베이스 탐색을 캡슐화한 것, 또는 그와 같은 관계에 견줄만한 구현을 한 것으로 볼 수 있다.

일대다(one-to-many) 연관관계는 어느 인스턴스의 필드로 컬렉션을 두는 것으로 구현할 수 있다. 하지만 반드시 그렇게 되는 것은 아니다. 필드없이 접근자 메서드에서 직접 데이터베이스를 조회하여 적절한 레코드를 찾아 이를 토대로 객체를 인스턴스화할 수도 있다. 이런 설계와 구현은 모두 동일한 모델을 반영한다. 설계에서는 특정 구현을 명시해야 하며, 어떤 형태로 구현하든 그러한 행위는 모델 내의 연관관계와 일치해야 한다.

현실에서는 수많은 다대다(many-to-many) 연관관계가 있는데, 상당수가 양방향 연관관계로 나타난다. 초기에 도메인 모델링의 결과로 나타나는 모델도 그와 같은 경향을 보인다. 하지만 이런 형태의 연관관계는 구현과 유지보수를 복잡하게 만든다.

연관관계를 좀 더 쉽게 다루는 방법으로 아래의 세 가지가 있다.

  1. 탐색 방향을 부여한다.
  2. 한정자(qualifier)를 추가하여 사실상 다중성(multiplicity)를 줄인다.
  3. 중요하지 않은 연관관계는 제거한다.

가능한 관계를 제약하는 것이 중요하다. 양방향 연관관계는 두 객체가 모두 있어야만 이해할 수 있다. 요구사항에서 두 방향으로부터 모두 탐색되어야 한다는 요건이 없을 경우에는, 어느 한 방향으로만 탐색 방향을 제약하면 상호의존성이 줄어들고 설계가 단순해진다.

00.png

국가와 역대 대통령에 대한 관계를 표현할 수 있는데, 이는 일대다 관계로 연관관계를 단순화시킬 수 있다. 대통령 이름으로 ‘이 대통령이 있던 나라가 어디입니까?’ 라고 물어보지는 않는다. 위와 같이 단방향으로 탐색 방향을 제한함으로써 설계가 단순해지고 도메인에 통찰력(연관관계에서 어느 특정 방향이 더 의미있고 중요하다는 점)을 반영하게 된다.

연관관계에서 탐색 방향은 도메인의 본연적인 특성을 드러낸다. 제약이 가해진 연관관계는 더 많은 도메인 지식과 실질적인 설계를 반영할 수 있다.

도메인을 깊이있게 이해하다보면 자주 한정적인 관계에 이른다. 대통령은 특정 시점에 한 명의 대통령만 있다. 이런 중요한 도메인 규칙을 명시적으로 모델에 포함시킨다면 다음과 같다.

01.png

만약 중요한 의미를 담고 있지 않는 연관관계라면 완전히 제거하는 것이 좋다.

이렇게 연관관계를 제약하면 설계가 단순해져 훨씬 더 구현이 용이해진다. 도메인의 특성이 드러나게끔 일관되게 제약하면 도메인 개념이 더욱 명확하게 반영되어지고 구현이 단순해진다.

양방향 연관관계가 도메인에 있어서 의미가 있고 요구사항에도 부합한다면 유지해야 한다.

제약조건은 모델과 구현에 포함되어 있어야 한다. 그와 같은 제약조건은 모델을 더 정확하게 하고, 구현을 더욱 쉽게 유지보수할 수 있도록 해준다.


Entity (엔티티)

수많은 객체는 본질적으로 속성이 아닌 연속성과 식별성이 이어지느냐를 기준으로 정의된다.

많은 객체들이 속성이 아닌 식별성에 의해 정의될 수 있다. ‘사람’을 나타내는 객체는 여러 시스템에서 그 객체의 형태가 서로 다를 수 있지만, 동일한 사람인지 아니면 다른 사람인지는 구분할 필요가 있다. 이런 개념적인 식별성은 객체와 해당 객체의 저장 형태, 구현 사이에서 일치해야 한다.

객체 모델링을 할 때 속성에 집중하곤 하지만, Entity의 근본적인 개념은 객체의 생명주기 내내 이어지는 추상적인 연속성이며 그러한 연속성은 여러 형태를 거쳐 전달된다는 것이다. 이런 객체는 객체의 속성을 자신의 주된 정의로 삼지 않는다. 객체 생명주기에 걸쳐 작용하는 식별성의 이어짐이 나타나며, 그 형태는 종종 다르게 나타나기도 한다.

어떤 객체를 해당 객체의 식별성으로 정의할 경우 그 객체를 Entity(엔티티)라고 한다. Entity는 자신의 생명주기 동안 형태와 내용은 바뀌더라도 연속성은 유지되어야 한다. 이런 객체를 추적하기 위해 식별성이 정의되어 있어야 한다. Entity 클래스 정의와 책임, 속성 및 연관관계는 Entity의 특정 속성보다는 정체성에 초첨을 맞추어야 한다. 클래스 정의를 단순히 하고 생명주기의 연속성과 식별성에 집중해야 한다.

모델 내의 모든 객체가 의미있는 식별성을 지닌 Entity인 것은 아니다.

식별성을 주된 정의로 삼고, 집중하기 위해 객체의 형태나 이력에는 상관없이 각 Entity 객체를 구별하기 위한 수단이 있어야 한다. 이러한 식별 수단의 구현 방법은 여러가지가 있겠지만 모델에서 이 식별성을 구분하는 방법과 일치해야 한다. 식별에 사용되는 속성은 시스템의 상태과 관계없이 시스템에서 유일해야 한다.

식별성은 원래 세상에 존재하는 것이 아닌, 필요에 의해 보충된 의미이다. 현실세계의 같은 사물을 표현한 것일지라도 도메인 모델에서 Entity로 표현되거나 표현되지 않을 수 있다. 식별성에 대한 정의는 모델로부터 나온다. 따라서 식별성을 정의하려면 도메인을 이해해야 한다.


Entity 모델링

Entity의 가장 기본적인 책임은 객체의 행위가 명확하고 예측 가능하도록 연속성을 확립하는 것이다. Entity를 모델링할 때, 속성이나 행위에 집중하기 보다는 가장 본질적인 특징(Entity를 식별하고 탐색하여 일치시키는데 사용하는)만으로 정의한다. 개념에 필수적인 것만 추가하고 그 행위에 필요한 속성만 추가한다.

그 밖의 것들은 검토하여 Entity와 연관관계에 있는 다른 객체로 옮기도록 한다. 이들은 다른 Entity가 되거나 Value Object가 될 것이다.


02.png

위의 그림에서 customerID는 Entity의 유일한 식별자이며, 전화번호와 주소(contact phone, contact address)는 이러한 Customer를 찾거나 일치 여부를 판단하는 데 사용된다. 이름(name)은 한 사람의 식별성을 정의하지는 않지만 간혹 식별성을 판단하는 수단의 일부로 사용될 수도 있다.

이에 따라 이름, 연락처 같은 속성을 Customer로 옮겼지만 이는 두 Customer 간의 식별성을 파악하기 위해 옮긴 것이다. 만약 Customer에 여러 전화번호가 있고 그 번호가 식별성과 관련이 없다면 Sales Contact에 그대로 있어야 한다.


Value Object (값 객체)

개념적 식별성이 없는 객체도 많은데, 이러한 객체는 사물의 어떤 특징을 묘사한다.

보통 식별성을 정의하고 추적, 관리하기 위해서는 분석적인 노력이 필요하다. 그런데 식별성이 필요없는 객체에 대해서도 일괄적으로 식별성을 도입할 경우 오해를 불러 일으킬 수 있다.

Entity의 식별성을 관리하는 일은 매우 중요하지만, 식별성이 필요없는 그 밖의 객체에 대해서도 식별성을 추가한다면 시스템의 성능이 저하되고, 분석작업이 별도로 필요하며, 모든 객체를 동일한 것처럼 보이게 해서 모델이 혼란스러워질 수 있다.

이런 식별성이 필요없는 객체는 사물을 서술하는 객체로, 모델에 중요한 의미를 갖고 있다. 개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 Value Object라고 부른다. 이런 객체는 이 객체가 어느 것인지에 대해서는 관심이 없고, 무엇인지에 대해서만 관심이 있다.

모델에 포함되는 요소들 중, 속성을 기술하는 데 중점을 둔다면 그것은 Value Object로 분류해야 한다. 속성의 의미를 잘 표현할 수 있도록 구현하고, 관련 행위나 기능을 부여하라. 그리고 Value Object는 아무런 식별성도 부여하지 말고, 불변적으로 다루어야 한다.

Value Object는 다른 객체를 속성으로 가지거나 풍부한 기능을 가질 수도 있으며, Entity를 참조할 수도 있다. 다만 식별성을 가지는 것이 의미가 없는 객체일 뿐이다.

Value Object는 여러 객체 간에 오가는, 메서드의 매개변수로 사용되기도 하며, 어떤 연산에서 임시로 사용될 목적으로 만들어진 후 폐기되기도 한다. 또한 Entity나 또다른 Value Object의 속성으로 사용되기도 한다.

Value Object를 구성하는 속성은 개념적 완전성을 형성해야 한다. 예를 들어 다음 그림의 왼쪽과 같이 Customer 객체에서 street, city, state와 같은 주소를 나타내는 속성은 개별 속성으로 표현될 필요가 없다. 이런 속성들은 오른쪽과 같이 하나의 완전한 Address(주소)를 구성함으로써 더 단순한 Customer와 더 응집력 있는 Value Object를 만들어낸다.

03.png


Value Object의 설계

Value Object를 사용함에 있어서 인스턴스의 복사나 공유, 불변성에 대한 의사결정이 필요하다.

두 사람의 이름이 같다고 해서 두 사람이 동일 인물이 되는 것은 아니다. 이런 이름을 나타내는 객체는 서로 바꿀 수 있는데, 이름에서는 오직 이름의 철자만이 중요하기 때문이다. 따라서 어느 사람을 나타내는 Person 객체에서 두 번째 Person 객체로 Name 객체를 복사할 수 있다.

이러한 Person 객체에서 각각 고유한 이름 인스턴스를 가질 필요도 없을지 모른다. 동일한 Name 객체는 두 Person 객체 간에 공유가 가능하며 두 Person 객체의 행위나 식별성은 영향이 없다. 그런데 이 상황에서 어느 한 Person 객체가 가리키는 Name 객체의 속성이 변경된다면 다른 사람의 이름까지 바뀌게 되므로 이 Name 객체는 불변적으로 다루어야 할 수 있다.

불변성은 한 객체가 해당 객체의 속성을 인자나 반환 값으로 다른 쪽으로 전달할 때 나타나는 문제를 방지할 수 있다. 객체의 소유자가 제어하지 못하는 이러한 떠돌이 객체로 인해 소유자의 불변식이 훼손될 수 있으므로, 속성을 전달할 때 방어적 복사나 불변 객체로 전달하여 방지하는 것이다.

복사나 공유 중 어느 것이 경제성 면에서 더 나은지는 구현 환경에 따라 달라진다. 복사의 경우 객체의 개수가 매우 많아져 시스템이 무거워질 수 있다.

Value Object가 불변적으로 다루어진다면 이 객체는 마음껏 공유할 수 있다. 달리 말하면, Value Object가 변경 가능하다면 공유해서는 안된다.

Value Object가 변경되는 것을 허용하는 경우는 새로운 Value Object를 매번 생성함으로서 나타나는 성능의 문제가 있을 경우에만 한해야 한다.


Value Object를 포함한 연관관계 설계

모델에 포함되는 요소들은 연관관계의 수가 적고 단순할수록 더 나은 모델이라 할 수 있다.

Entity 간의 양방향 연관관계는 필요에 따라 존재할 수 있지만, Value Object 간의 양방향 연관관계는 논리적으로 타당하지 않다. 어떤 객체가 식별성 없이 자신을 가리키는 동일한 Value Object를 역으로 가리키는 것은 아무런 의미가 없다. Value Object끼리의 양방향 연관관계에 대한 유용한 예도 별로 없다. 따라서 Value Object 간의 양방향 연관관계는 없다고 봐도 좋다.


Entity와 Value Object

Entity와 Value Object의 분리는 도메인 개념들의 추적성, 연속성 / 식별성을 추상화하기 위한 분석 기법이다. 도메인 영역의 개념들을 Entity와 Value Object로 분리함으로써 도메인의 본질적인 특성에 맞추게 된다.

고객은 유일한가? 고객이 유일하게 구분되는 특성은 무엇인가? 시스템은 고객의 주문이나 구매 기록을 추적하기 위해 어떤 처리를 해야 하는가? 금액은 유일할 필요가 없고 단순하게 값만 비교하면 되는가?

도메인 개념에 대한 이런 질문들은 도메인에 대한 이해를 촉진시키고 소프트웨어의 복잡성을 완화하는 유용한 분석 기법이다. Entity를 식별함으로써 핵심 개념들의 생명 주기에 초점을 맞출 수 있다. Value Object를 식별함으로써 도메인의 일부이지만 중요하지 않은 개념들을 걸러낼 수 있다.


Service (서비스)

도메인 모델의 각 요소들 중 개념적으로는 어떠한 객체에도 속하지 않는 연산이 포함될 때가 있다. 이러한 연산은 특정 Entity나 Value Object의 연산이 아닌 경우이다. 이런 연산은 본질적으로 사물로 표현되지 않는 활동이나 행동인데 이러한 연산도 객체와 잘 어울리게끔 노력해야 한다.

하지만 특정 Entity나 Value Object에 종속되지 않는 이러한 연산들을 강제로 그 객체에 포함시킨다면 해당 객체는 자신의 개념적 명확성을 잃어버리고 이해하거나 리팩터링하기가 어려워진다. 이럴 경우 해당 객체의 역할을 불분명하게 만들 수 있다. 이 연산의 특징은 자신의 상태가 없고 도메인에서 맡고 있는 연산 이상으로는 어떠한 의미도 가지지 않는 경우가 많다. 이런 기능을 Entity나 Value Object에서 억지로 맡게 된다면 모델에 기반을 둔 그 객체의 정의가 왜곡되거나 무의미하고 인위적으로 만들어진 객체가 추가될 수 있다.

보통 이런 연산들은 여러 도메인 객체들을 모아 그들을 조율해나가면서 어떤 행위가 일어나게 하므로, 그 연산을 특정 객체에 추가시킨다는 것은 그 객체가 다른 도메인 객체에 대해 의존성을 만들어내는 것이다.

Service는 모델에서 독립적인 인터페이스로 제공되는 연산으로 Entity나 Value Object와 같이 상태를 캡슐화하지는 않는다. 단지 행위만 모델링 할 뿐이다. 이 Service라는 이름은 다른 객체와의 관계를 강조하며, Entity나 Value Object와는 달리 정의하는 기준이 순전히 클라이언트에게 무엇을 제공할 수 있느냐에 있다.

Service는 주로 활동으로 이름을 지으며, Service에 부여된 책임과 행위 및 인터페이스는 도메인 모델의 일부로서 정의될 수 있다. 당연히 연산의 명칭은 Ubiquitous Language에서 가져와야 하며 Service의 매개변수와 결과는 도메인 객체여야 한다.

Service에서 구현되는 행위는 Entity나 Value Object에서 수행될 수 있는 행위여서는 안된다.

잘 만들어진 Service에는 아래의 세 가지 특징이 있다.

  1. 연산이 Entity나 Value Object의 일부를 구성하는 것이 아니지만, 도메인 개념과 관련이 있다.
  2. 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다.
  3. 연산이 상태를 갖지 않는다.

도메인의 중요한 연산이나 과정이 Entity나 Value Object의 고유한 책임이 아니면 Service로 선언되는 독립 인터페이스로서 모델에 추가시키도록 한다. 그리고 Service는 상태를 가져서는 안된다.


Service와 격리된 도메인 계층

Service는 도메인 계층에서만 이용되는 것이 아니다. 도메인 계층에 속하는 Service와 다른 계층에 속하는 Service들을 구분하고 책임을 나누는데 주의를 기울여야 한다.

도메인 Service와 응용 Service는 인프라스트럭처 계층의 Service와 협업하도록 구현한다.

응용 Service와 도메인 Service의 구분은 도메인과 관련된 업무 규칙을 포함하고 있느냐 없느냐에 달려 있다. 인프라스트럭처 Service는 단순히 기술과 관련된 것만 구현하며 업무와 관련된 어떠한 것도 포함되어서는 안된다.

  • 은행 시스템에서 서비스를 여러 계층으로 분할하기
    • 응용 Service: 은행 업무 규칙, 은행 도메인과 관련이 없음
      • 사용자 입력의 암호화
      • 고객에 이메일 발송
      • 도메인 객체의 행위를 조정
      • 도메인 Service 호출
    • 도메인 Service: 업무 규칙, 도메인 기능을 포함
      • 자금 이체
      • 계좌 잔고 확인
      • 도메인 객체들을 조율하여 하나의 업무 프로세스를 진행하는 로직
    • 인프라스트럭처 Service: 기술적인 구현을 포함
      • 애플리케이션에서 이메일이나 우편을 보내는 기술적인 구현

도메인 Service는 도메인 개념을 행위로 모델링하는 역할뿐만 아니라, Entity와 Value Object로부터 클라이언트를 분리하는 것으로, 도메인 계층의 인터페이스 구성 단위를 제어하는 수단으로서도 가치가 있다. 이는 응용이나 도메인, 인프라스트럭처 계층 간의 경계를 선명하게 하는 데 도움될 수 있는데, 예를 들어 구성 단위가 세밀한 작은 도메인 객체들의 복잡한 상호작용은 이들을 조정하는 응용 계층에서 처리되어, 결국 응용 계층으로 도메인 지식이 새어나갈 수 있다. 이 때 도메인 Service를 적절히 도입하면 계층 간의 경계를 선명하게 하는 데 도움이 될 수 있다.


모듈

Module은 오래 전부터 확립되어 사용되고 있는 설계 요소다. 모듈화하는 가장 주된 이유는 인지적 과부화(cognitive overload)때문이다. 사람들은 Module을 토대로 모델을 두 가지 측면에서 바라볼 수 있다. 사람들은 전체에 압도되지 않고 Module에 들어 있는 세부사항을 보거나, Module에 있는 세부사항을 배제한 상태에서 Module 간의 관계를 볼 수 있다.

Module 간에는 결합도가 낮아야 하고, Module의 내부는 응집도가 높아야 한다. Module로 쪼개지는 기준은 코드가 아닌 바로 개념이다. 사람은 한 번에 생각할 수 있는 양에는 한계가 있으며(결합도를 낮춰야 한다), 일관성이 없는 단편적인 생각은 이해하기 어렵다.(응집도는 높여야 한다)

낮은 결합도와 높은 응집도는 개별 객체에서와 마찬가지로 Module에도 적용되는 일반적인 설계 원칙이며, 그 원칙은 구성 단위가 큰 모델링과 설계에서는 특히 중요하다.

Module과 좀 더 규모가 작은 요소들은 함께 발전해나가야 하지만, 대게 그렇게 되지 않는다. Module 전체를 리팩터링하는 것은 일반 클래스를 리팩터링하는 것보다 일이 더 많고 파급효과가 커서 자주 하기는 힘들기 때문이다. 하지만 모델 객체가 지식 탐구를 통해 점차 정제되어 더욱 심층적인 통찰력을 드러내는 것처럼, Module도 정제되고 발전될 수 있다. 도메인을 이해한 바에 따라 이를 Module에도 반영하면 Module 안의 객체도 자연스럽게 발전할 수 있다.

도메인 주도 설계의 다른 모든 것들과 마찬가지로 Module도 하나의 의사소통 매커니즘이다. 분할되는 객체의 의미에 따라 Module을 선택해야 한다. 일련의 응집력있는 개념들을 하나의 Module에 담아야 한다. Module 간의 결합도가 낮아지지 않는다면, 모델을 변경해서 얽혀있는 개념을 풀어낼 방법을 찾아보거나 의미있는 방식으로 미처 못보고 지나간 개념을 찾아보라. 높은 수준의 도메인 개념에 따라 모델이 분리되고 그것에 대응되는 코드도 분리될 때까지 모델을 정제해나가야 한다.

객체의 가장 기본적인 개념 중 하나는 데이터와 해당 데이터를 대상으로 연산을 수행하는 로직을 캡슐화하는 것이다. 마찬가지로 하나의 개념을 구현하는 코드들은 모두 같은 Module에 두어야 한다.


Tags:
Stats:
0 comments