04장 도메인의 격리
계층형 아키텍처 (Layered Architecture)
보조적인 성격의 코드를 비즈니스 객체 안에 직접 작성할 경우
도메인에 관련된 코드가 상당한 양의 도메인과 관련이 없는 다른 코드를 통해 널리 확산될 경우 도메인에 관련된 코드를 확인하고 추론하기가 굉장히 힘들어진다. 그리고 응집력 있고, 모델 주도적인 객체를 구현하는 것이 비현실적인 이야기가 돼버리고 자동화 테스트가 어려워진다.
매우 복잡한 작업을 처리하는 소프트웨어를 만들 경우 관심사의 분리(separation of concern)가 필요하다.
계층화의 핵심 원칙
- 한 계층의 모든 요소는 오직 같은 계층에 존재하는 다른 요소나 계층상 아래에 위치한 요소에만 의존
- 위로 거슬러 올라가는 통신은 반드시 간접적인 메커니즘을 거쳐야 함
계층화의 가치
- 응집력 있는 설계가 가능해짐
- 설계를 훨씬 더 쉽게 이해할 수 있음
Layered Architechture의 네 가지 개념적 계층
사용자 인터페이스 (표현 계층)
- 사용자에게 정보를 보여주고 사용자의 명령을 해석하는 일을 책임진다.
- Controller
응용 계층
- 소프트웨어가 수행할 작업을 정의하고 표현력 있는 도메인 객체가 문제를 해결하게 한다.
- 업무 규칙이나 지식이 포함되지 않으며, 오직 작업을 조정하고 아래에 위치한 계층에 포함된 도메인 객체의 협력자에게 작업을 위임한다.
- Service
도메인 계층 (모델 계층)
- 업무 개념과 업무 상황에 관한 정보, 업무 규칙을 표현하는 일을 책임진다.
- 업무 상황을 반영하는 상태를 제어하고 사용하며, 상태 저장과 관련된 기술적인 세부사항은 인프라스트럭쳐에 위임한다.
- Domain
인프라스트럭쳐 계층
- 상위 계층을 지원하는 일반화된 기술적 기능을 제공한다.
- 도메인 계층에서 어떤 활동이 일어나게 하지 않는다.
- 해당 인프라스트럭쳐 계층이 보조하는 도메인의 구체적인 지식을 가져서는 안 된다.
- 메시지 전송, 도메인 영속화 등등
- Repository, SMS, Notification, Email...
Model Driven Design을 가능케 하는 것은 결정적으로 도메인 계층을 분리하는 데 있다.
Layered Architechture의 주요 원칙
- 복잡한 프로그램을 여러 개의 계층으로 나눠라
- 응집력 있고 오직 아래에 위치한 계층에만 의존하는 각 계층에서 설계를 발전시켜라
- 표준 아키텍처 패턴에 따라 상위 계층과의 결합을 느슨하게 유지하라
- 도메인 모델과 관련된 코드는 모두 한 계층에 모으고, 사용자 인터페이스 코드나 애플리케이션 코드, 인프라스트럭쳐 코드와 격리하라
➡️ 모델은 진화를 거듭해 본질적인 업무 지식을 포착해서 해당 업무 지식이 효과를 발휘할 수 있을 만큼 풍부하고 명확해짐
계층 간 관계 설정
각 계층은 설계 의존성을 오직 한 방향으로만 둬서 느슨하게 결합된다.
- 상위 계층은 하위 계층의 공개 인터페이스를 호출하고 하위 계층에 대한 참조를 가짐
- 하위 수준의 객체가 상위 수준의 객체와 소통해야 할 경우, 콜백이나 옵서버 패턴처럼 계층 간에 관계를 맺어주는 아키텍처 패턴을 활용
05장 소프트웨어에서 표현되는 모델
연관관계
- 가능한 한 관계를 제약하는 것이 중요하다.
- 애플리케이션 요구사항에 두 방향을 모두 탐색해야 한다는 요건이 없을 경우 탐색 방향을 추가하면 상호의존성이 줄어들고 설계가 단순해진다.
- 도메인을 이해하면 도메인 본연의 방향성이 드러날지도 모른다.
아래 예시는 사람과 국가를 대통령이라는 관계로 이어주고 있는 가운데 도메인 본연의 방향성을 드러내는 예시입니다.
위 두 개의 예시를 비교했을 때, 대부분의 경우 후자와 같은 방식으로 물어볼 것입니다.
때문에 실용적인 관점에서 본다면 기존의 양방향 관계에서 사람 → 국가 방향의 연관관계를 제거할 수 있습니다.
다대다 연관관계의 탐색 방향을 제약하면 해당 연관관계는 사실상 훨씬 더 구현하기 쉬운 일대다 연관관계로 줄어듭니다.
도메인의 특성이 반영되게끔 연관관계를 일관되게 제약하면 연관관계의 의사전달력이 풍부해지고 구현이 단순해지며, 나머지 양방향 연관관계도 의미를 지니게 됩니다.
엔티티 (Entity)
개념적 식별성은 객체와 해당 객체의 저장 형태, 현실의 행위자의 구현 사이에서 일치해야 하며, 속성은 일치하지 않을 수도 있다.
엔티티의 근본적인 개념은 객체의 생명주기 내내 이어지는 추상적인 연속성이며, 그러한 추상적인 연속성은 여러 형태를 거쳐 전달된다는 것이다.
어떤 객체를 일차적으로 해당 객체의 식별성으로 정의할 경우 그 객체를 엔티티라 한다.
- 엔티티에는 모델링과 설계상의 특수한 고려사항이 포함돼 있다.
- 엔티티는 자신의 생명주기 동안 형태와 내용이 급격하게 바뀔 수도 있지만 연속성은 유지해야 한다.
- 사실상 엔티티를 추적하려면 엔티티에 식별성이 정의돼 있어야 한다.
- 엔티티의 클래스 정의와 책임, 속성, 연관관계는 엔티티에 포함된 특정 속성보다는 엔티티의 정체성에 초점을 맞춰야 한다.
- 엔티티가 그렇게까지 급격하게 변형되지 않거나 생명주기가 복잡하지 않더라도 의미에 따라 엔티티를 분류한다면 모델이 더욱 투명해지고 구현은 견고해질 것이다.
엔티티의 식별성
- 한 객체가 속성보다는 식별성으로 구분될 경우 모델 내에서 이를 해당 객체의 주된 정의로 삼아라.
- 클래스의 정의를 단순하게 하고 생명주기의 연속성과 식별성에 집중하라.
- 객체의 형태나 이력에 관계없이 각 객체를 구별하는 수단을 정의하라.
- 객체의 속성으로 객체의 일치 여부를 판단하는 요구사항에 주의하라.
- 각 객체에 대해 유일한 결과를 반환하는 연산을 정의하라.
- 식별 수단은 외부에서 가져오거나 시스템에서 자체적으로 만들어 내는 임의의 식별자일 수도 있지만, 모델에서 식별성을 구분하는 방법과 일치해야 한다.
- 모델은 동일하다는 것이 무슨 의미인지 정의해야 한다.
엔티티 모델링
- 엔티티의 가장 기본적인 책임은 객체의 행위가 명확하고 예측 가능해질 수 있게 연속성을 확립하는 것이다.
- 엔티티는 별도로 분리돼 있을 때 자신의 책임을 가장 잘 수행한다.
- 엔티티의 속성이나 행위에 집중하기보다는 엔티티 객체를 해당 엔티티 객체의 가장 본질적인 특징만으로 정의한다.
- 개념에 필수적인 행위만 추가하고 그 행위에 필요한 속성만 추가한다.
- 그 밖의 객체는 행위와 속성을 검토해서 가장 중심이 되는 엔티티와 연관관계에 있는 다른 객체로 옮긴다.
값 객체 (Value Object)
엔티티의 식별성을 관리하는 일은 매우 중요하지만 그 밖의 객체에서 식별성을 추가한다면 시스템의 성능이 저하되고, 분석 작업이 별도로 필요하며, 모든 객체를 동일한 것으로 보이게 해서 모델이 혼란스러워질 수 있다.
개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 Value Object라고 한다.
Value Object는 설계 요소를 표현할 목적으로 인스턴스화 되는데, 우리는 이러한 설계 요소가 어느 것인지에 대해서는 관심이 없고 오직 해당 요소가 무엇인지에 대해서만 관심이 있다.
- 모델에 포함된 어떤 요소의 속성에만 관심이 있다면 그것을 Value Object로 분류하라.
- Value Object가 전하는 속성의 의미를 표현하게 하고 관련 기능을 부여하라.
- Value Object는 불변적으로 다뤄라.
- Value Object에 아무런 식별성을 부여하지 말고 엔티티를 유지하는 데 필요한 설계상의 복잡성을 피하라.
서비스 (Service)
객체의 정의에 어울리지 않는 연산을 강제로 객체에 포함시킨다면 해당 객체는 자신의 개념적 명확성을 잃어버리고 이해하거나 리팩터링 하기 힘들어질 것이다.
서비스는 모델에서 독립적인 인터페이스로 제공되는 연산으로서 엔티티나 값 객체와 달리 상태를 캡슐화하지 않는다.
엔티티나 값 객체와 달리 서비스를 정의하는 기준은 순전히 클라이언트에 무엇을 제공할 수 있느냐에 있다.
엔티티가 주로 동사나 명사로 이름을 부여하는 것과 달리 서비스는 주로 활동으로 이름을 짓는다.
연산의 명칭은 UBIQUITOUS LANGUAGE에서 유래하거나 UBIQUITOUS LANGUAGE에 도입돼야 한다.
또한 서비스의 매개변수와 결과는 도메인 객체여야 한다.
잘 만들어진 서비스의 특징
- 연산이 원래부터 엔티티나 값 객체의 일부를 구성하는 것이 아니라 도메인 개념과 관련돼 있다.
- 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다.
- 연산이 상태를 갖지 않는다. → 클라이언트가 특정 서비스 인스턴스의 개별 이력과는 상관없이 서비스의 모든 인스턴스를 사용할 수 있음
도메인의 중대한 프로세스나 변환 과정이 엔티티나 값 객체의 고유한 책임이 아니라면 연산을 서비스로 선언되는 독립 인터페이스로 모델에 추가하라.
모델의 언어라는 측면에서 인터페이스를 정의하고 연산의 이름을 UBIQUITOUS LANGUAGE의 일부가 되게끔 구성하라.
서비스는 상태를 갖지 않게 만들어라.
모듈 (Module, Package)
모듈화의 가장 주된 이유는 바로 인지적 과부하(Cognitive Overload)이다.
어떤 사람이 한 번에 생각해낼 수 있는 양에는 한계가 있으며, 일관성이 없는 단편적인 생각은 획일적인 생각을 섞어놓은 것처럼 이해하기 어렵다. 따라서 모듈 간에는 결합도가 낮아야 하고, 모듈의 내부는 응집도가 높아야 한다.
모듈은 객체의 초기 형태를 조직화할 목적으로 사용된다. 그러고 나면 객체는 기존 모듈이 정의한 범위 안에 머무를 수 있는 방식으로 변화한다. 모듈을 리팩터링 하는 것은 클래스를 리팩터링하는 것보다 일이 더 많고, 파급효과가 더 크며, 아마도 자주 하기는 힘들 것이다.
그러나 모델 객체가 원시적이고 구체적인 상태에서 시작해 점차 심층적인 통찰력을 드러내는 것처럼 모듈도 정교해지고 추상적인 형태로 변화할 수 있다.
어떤 클래스들을 한 모듈 안에 함께 둔다면 그것은 바로 여러분 옆에서 설계를 살펴보는 동료 개발자에게 그 클래스들을 하나로 묶어서 생각하자고 말하는 것과 같다.
서로 독립적으로 이해하고 논리적으로 추론할 수 있다는 의미에서 낮은 결합도가 달성되도록 노력하라.
높은 수준의 도메인 개념에 따라 모델이 분리되고 그것에 대응되는 코드도 분리될 때까지 모델을 정제하라.
기술적인 정교함이 주도하는 패키지화 계획에 따르는 비용
- 프레임워크의 분할 관례 탓에 개념적 객체를 구현하는 요소가 서로 떨어져 있으면 더는 코드에서 모델이 드러나지 않는다.
- 머릿속으로 다시 합칠 수 있는 만큼밖에 분할돼 있지 않은데 프레임워크에서 그렇게 분할된 결과를 모조리 사용해버리면 도메인 개발자들은 모델을 의미 있는 조각으로 나누는 능력을 잃어버리게 된다.
하나의 개념적 객체를 구현하는 코드는 모두 같은 MODULE에 둬야 한다.
구현에서의 객체와 포인터, 검색 메커니즘은 모델 요소에 직접적이고 분명하게 매핑돼야 한다.
어떤 것의 개념이 도메인 객체와 밀접하게 관련돼 있지 않다면 그것을 도메인 객체에 추가해서는 안 된다.
모델링 패러다임
객체 패러다임이 지배적인 이유
- 객체 모델링이 복잡함과 단순함의 절묘한 조화를 이룬다.
- 객체 모델링의 개념은 단순하지만 중요한 도메인 지식을 포착할 만큼 풍부한 것으로 입증됐다.
객체 세계에서 객체가 아닌 것들
//TODO
객체가 아닌 요소를 객체지향 시스템에 혼합하는 법칙
- 구현 패러다임을 도메인에 억지로 맞추지 않는다.
도메인에 관한 사고방식은 반드시 하나만 있는 것이 아니다. 패러다임에 어울리는 모델 개념을 찾는다. - 유비쿼터스 언어에 의지한다.
- UML에 심취하지 않는다.
- 회의적이어야 한다.
06장 도메인 객체의 생명주기
도메인 객체의 관리와 관련된 문제는 아래 두 가지 범주로 나뉜다.
- 생명주기 동안의 무결성 유지하기
- 생명주기 관리의 복잡성으로 모델이 난해 해지는 것을 방지하기
이러한 문제는 세 가지 패턴을 통해 해결할 수 있다.
- AGGREGATE(집합체)는 소유권과 경계를 명확히 정의함으로써 모델을 엄격하게 만들어 객체 간의 연관관계가 혼란스럽게 얽히지 않게 한다.
- FACTORY(팩토리)를 이용해 복잡한 객체와 AGGREGATE를 생성하고 재구성함으로써 그것들의 내부 구조를 캡슐화한다.
- REPOSITORY(리파지터리)를 통해 영속 객체를 찾아 조회하는 수단을 제공한다.
AGGREGATE (집합체)
AGGREGATE는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음을 말하며, 각 AGGREGATE에는 루트와 경계가 있다.
- 경계 : AGGREGATE에 무엇이 포함되고 포함되지 않는지를 정의
- 루트 : 단 하나만 존재하며, AGGREGATE에 포함된 특정 ENTITY를 가리킨다.
경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 AGGREGATE의 구성요소 가운데 루트만 참조할 수 있다.
루트 이외의 ENTITY는 지역 식별성을 지니며, 지역 식별성은 AGGREGATE 내에서만 구분되면 된다. 해당 AGGREGATE의 경계 밖에 위치한 객체는 루트 ENTITY의 콘텍스트 말고는 AGGREGATE의 내부를 볼 수 없기 때문이다.
❓ 그렇다면 한 AGGREGATE 안의 루트 ENTITY를 제외한 다른 ENTITY를 외부에서 참조해야 한다면 별도 AGGREGATE로 분리해야 하는 것일까?
❗️ 그렇지 않다. AGGREAGATE를 정상적으로 구성한 경우 루트 ENTITY를 통해 참조하면 된다. 만약 루트 ENTITY와 해당 ENTITY가 각각의 생명 주기, 데이터 변경의 단위를 가지고 있다면 별도 AGGREGATE로 분리하는 것이 맞을 것이다.
불변식
- 데이터가 변경될 때마다 유지돼야 하는 일관성 규칙
- AGGREGATE를 구성하는 각 구성요소 간의 관계도 포함
- 한 AGGREGATE에 적용된 불변식은 각 트랜잭션이 완료될 때 이행
개념적 AGGREGATE를 구현하기 위한 트랜잭션 규칙
- 루트 ENTITY는 전역 식별성을 지니며 궁극적으로 불변식을 검사할 책임이 있다.
- AGGREGATE 경계 밖에서는 루트 ENTITY를 제외한 AGGREGATE 내부의 구성요소를 참조할 수 없다.
루트 ENTITY가 내부 ENTITY에 대한 참조를 다른 객체에 전달해 줄 수는 있지만 일시적으로만 사용할 수 있고, 참조를 계속 보유하고 있을 수는 없다. - 데이터베이스 질의를 이용하면 AGGREGATE의 루트만 직접적으로 획득할 수 있다.
다른 객체는 모두 AGGREGATE를 탐색해서 발견해야 한다. - 삭제 연산은 AGGREGATE 경계 안의 모든 요소를 한 번에 제거해야 한다.
- AGGREGATE 경계 안의 어떤 객체를 변경하더라도 전체 AGGREGATE의 불변식은 모두 지켜져야 한다.
FACTORY (팩토리)
- 자신의 책임이 다른 객체를 생성하는 것인 프로그램 요소.
- 도메인 모델에서 아무런 책임도 맡지 않을 수도 있지만 여전히 도메인 설계의 일부를 구성한다.
- 복잡한 객체나 AGGREGATE를 생성하는 데 필요한 지식을 캡슐화한다.
- 클라이언트의 목적과 생성된 객체의 추상적인 관점을 반영하는 인터페이스를 제공한다.
클라이언트에서 자신이 원하는 도메인 객체를 직접 조립해야 한다면?
- 클라이언트는 도메인 객체의 내부 구조를 어느 정도 알아야 한다.
- 모든 불변식을 이행하려면 클라이언트에서 해당 객체의 규칙을 어느 정도 알아야 한다.
- 클라이언트를 변경하지 않고는 도메인 객체의 구현을 변경할 수 없으며, 이로써 리팩터링이 더 힘들어진다.
- 객체 생성을 맡은 클라이언트는 불필요하게 복잡해지고 클라이언트가 맡고 있는 책임은 불분명해진다.
- 클라이언트는 도메인 객체와 생성된 AGGREGATE의 캡슐화를 위반한다.
FACTORY를 잘 설계하기 위한 두 가지 기본 요건
- 각 생성 방법은 원자적이어야 하며, 생성된 객체나 AGGREGATE의 불변식을 모두 지켜야 한다.
- FACTORY는 생성된 클래스보다는 생성하고자 하는 타입으로 추상화돼야 한다.
❓ Entity를 생성한다면 인터페이스를 추가해서 사용해야 할까?
❗️ 클래스가 타입이며 계층 구조나 인터페이스를 구현하지는 않기 때문에 생성자로 충분하다.
FACTORY의 위치 선정
- FACTORY는 해당 FACTORY에서 만들어내는 객체와 매우 강하게 결합돼 있으므로 FACTORY는 자신의 생성물과 가장 밀접한 관계에 있는 객체에 있어야 한다.
- 특정 AGGREAGATE 안의 어떤 객체가 FACTORY를 필요로 하는데 AGGREGATE 루트가 해당 FACTORY가 있기에 적절한 곳이 아니라면 독립형 FACTOR를 만들면 된다.
생성자만으로 충분한 경우
- 클래스가 타입인 경우
- 클라이언트가 STRATEGY를 선택하는 한 방법으로서 구현체에 관심이 있는 경우
- 클라이언트에게 노출된 생성자 내에서 객체 생성이 중첩되지 않는 경우
- 생성자가 복잡하지 않은 경우
- 공개 생성자가 FACTORY와 동일한 규칙을 반드시 준수해야 하는 경우
인터페이스 설계
- 각 연산은 원자적이어야 한다.
일관성을 지키고자 FACTORY에서 발생하는 실패에 대해 코딩 표준을 도입할 것을 고려해본다. - FACTORY는 자신에게 전달된 인자와 결합될 것이다.
인자가 단순히 생성물에 들어가는 것이라면 가장 의존성이 적당한 상태다. 그러나 인자를 끄집어내서 객체 생성 과정에 사용한다면 결합은 더 강해진다. - 구상 클래스가 아닌 추상적인 타입의 인자를 사용하라.
FACTORY는 생성물의 구상 클래스에 결합되므로 구상 매개변수에도 결합될 필요는 없다.
불변식 로직의 위치
- FACTORY는 불변식 검사를 생성물에 위임할 수 있으며, 간혹 이렇게 하는 것이 최선일 때도 있다.
- 불변식 로직을 FACTORY에 둬서 생성물에 들어 있는 복잡한 요소를 줄이는 것도 이점이 있다.
하지만 다른 도메인 객체에 속한 FACTORY METHOD의 경우에는 그렇지 못하다.
REPOSITORY (리파지터리)
- 영속화된 객체를 획득하고 해당 객체의 생명주기를 관리하기 위한 단순한 모델을 클라이언트에게 제시한다.
- 영속화 기술과 다수의 데이터베이스 전략, 또는 심지어 다수의 데이터 소스로부터 애플리케이션과 도메인 설계를 분리해준다.
- 객체 접근에 관한 설계 결정을 전해준다.
- 테스트에서 사용할 가짜 구현을 손쉽게 대체할 수 있다.
Reference
'🏗️ ARCHITECTURE' 카테고리의 다른 글
Onion Architecture란? (0) | 2022.03.20 |
---|---|
Facade Pattern과 API Composition (0) | 2022.03.13 |
[도메인 주도 설계] 1부 : 동작하는 도메인 모델 만들기 (0) | 2022.02.01 |
[12 Factor App] 마이크로서비스 개발 전 읽어보세요 (0) | 2021.06.07 |
[나의 MSA 구축 일기] 처음 MSA를 구축해보며 겪었던 시행착오들 (4) | 2021.02.21 |