본문 바로가기
Book

[독서 기록] 도메인 주도 설계, 에릭 에반스

by Renechoi 2024. 1. 23.
 
도메인 주도 설계
소프트웨어의 복잡성을 다루는 지혜『도메인 주도 설계』. 이 책은 독자에게 도메인 주도 설계에 대한 체계적인 접근법을 제공하고 폭넓은 우수 설계 실천법과 경험을 토대로 한 기법, 복잡한 도메인에 직면한 소프트웨어 프로젝트의 발전을 가능하게 하는 근본 원칙을 제시한다. 설계 및 개발 원칙들을 실은 이 책은 현실세계의 소프트웨어 개발에 도메인 주도 설계를 응용한 모습을 생생하게 보여주는 실제 프로젝트에 기반한 수많은 예제들을 실었다.
저자
에릭 에반스
출판
위키북스
출판일
2011.07.21

 

도메인 주도 설계, 에릭 에반스

 

 

 

 

수많은 어플리케이션에서 가장 중요한 복잡성은 기술적인 것이 아니다. 그것은 바로 사용자의 활동이나 업무에 해당하는 도메인 자체다.

- xxxiii

 

 

이 책의 전제는 다음과 같다.

  1. 대부분의 소프트웨어 프로젝트에서는 가장 먼저 도메인과 도메인 로직에 집중해야 한다.
  2. 복잡한 도메인 설계는 모델을 기반으로 해야 한다. 

- xxxiii

 

사용자의 활동에 도움되는 소프트웨어를 만들기 위해 개발팀은 사용자의 활동과 관련된 지식 체계에 집중해야 한다. 이를 위해 갖춰야 할 지식의 폭은 위압적일 수 있다. 개발팀은 그러한 정보의 양과 복잡성에 압도될 수 있다. 모델은 이러한 부담을 해소하기 위한 도구다. 모델은 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태다. 우리는 적절한 모델을 토대로 정보를 이해하고 문제 자체에 집중할 수 있다.

- 3

 

도메인 모델링은 어떤 목적에 따라 제약에 구애받지 않고 현실을 표현하는 영화제작에 더 가깝다. 다큐멘터리 영화에서도 실생활을 편집해서 보여준다.

- 3

 

 

기술자들은 자신의 기술력을 훈련할 수 있는 정량적인 문제를 좋아한다. 도메인 업무는 무질서하고 컴퓨터 과학자로서의 능력에 보탬이 될 것 같지 않은 복잡하고 새로운 지식을 많이 요구한다.
-4

 

 

지식 탐구는 혼자서 하는 활동이 아니다. 개발자와 도메인 전문가로 구성된 팀은 대체로 개발자가 이끄는 가운데 협업한다.
-14

 

public int makeBooking(Cargo cargo, Voyage voyage){
    double maxBooking = voyage.capacity() * 1.1;
    if (voyage.bookedCargoSize() + cargo.size())> maxBooking)
      return -1;
   int confirmation = orderConfirmationSequence.next();
   voyage.addCargo(cargo, confirmation);
    return confirmationl
}

... 현재 코드에는 다음과 같은 문제가 있다.

  1. 코드가 작성된 대로라면 개발자의 도움이 있더라도 업무 전문가가 이 코드를 읽고 규칙을 검증하지는 못할 것이다.
  2. 해당 업무에 종사하지 않고 기술적인 측면만 담당하는 사람은 코드와 요구사항을 결부시키기가 어려울 것이다.

...

코드는 이제 다음처럼 바뀐다.

public int makeBooking(Cargo cargo, Voyage voyage){
   if (!overbookingPolicy.isAllowd(cargo, voyage)) return -1;
   int confirmation = orderConfirmationSequence.next();
   voyage.addCargo(cargo, confirmation);
    return confirmationl
}

이렇게 하면 초과예약이 별개의 정책이라는 사실을 모든 이가 분명히 알게 될 것이며, 이 규칙의 구현 또한 명시적으로 드러나고 다른 구현과 분리된다.

- 19~20

 

 

유연하고 풍부한 지식이 담긴 설계를 만들려면 다용도로 사용할 수 있는 팀의 공유 언어와 그 언어에 대한 활발한 실험이 필요하다.

- 24

 

"Routing Service에 출발지, 목적지, 도착 시간을 전달하면 화물이 멈춰야 할 지점을 찾고 , 그것을 데이터베이스에 삽입한다" (모호하고 기술적임)

 

"출발지, 목적지, 등등... 이것들을 모두 Routing Service에 넣으면 필요한 것이 모두 담긴 Itinerary를 돌려받는다" (좀 더 완전해졌지만 장황함)

 

"Routing Service는 Route Specification을 만족하는 Itinerary를 찾는다." (간결함)

- 31

 

 

4장 도메인의 격리

 

도메인에 관련된 코드가 상당한 양의 도메인과 관련이 없는 다른 코드를 통해 널리 확산될 경우 도메인에 관련된 코드를 확인하고 추론하기가 굉장히 힘들어진다.

- 71

 

 

계층화의 핵심 원칙은 한 계층의 모든 요소는 오직 같은 계층에 존재하는 다른 요소나 계층상 '아래'에 위치한 요소에만 의존한다는 것이다. 위로 거슬러 올라가는 의사소통은 반드시 간접적인 메커니즘을 거쳐야 하며, 이러한 간접적인 메커니즘에 관해서는 나중에 논의하겠다.

-71

       
사용자 인터페이스 (또는 표현 계층) 사용자에게 정보를 보여주고사용자의 명령을 해석하는 일을 책임진다. 간혹 사람이 아닌 다른 컴퓨터 시스템이 외부 행위자가 되기도 한다.    
응용 계층 소프트웨어가 수행할 작업을 정리하고 표현력 있는 도메인 객체가 문제를 해결하게 한다. 이 계층에서 책임지는 작업은 업무상 중요하거나 다른 시스템의 응용 계층과 상호작용하는 데 필요한 것들이다. 이 계층은 얇게 유지된다. 여기에는 업무 규칙이나 지식이 포함되지 않으며, 오직 작업을 조정하고 아래에 위치한 계층에 포함된 도메인 객체의 협렵작에게 작업을 위임한다. 응용 계층에서는 업무 상황을 반영하는 상태가 없지만 사용자나 프로그램 작업에 대한 진행상황을 반영하는 상태를 가질 수는 있다.    
도메인 계층 업무 개념과 업무 상황에 관한 정보, 업무 규칙을 표현하는 일을 책임진다. 이 계층에서는 업무 상황을 반영하는 상태를 제어하고 사용하며, 그와 같은 상태 저장과 관련된 기술적인 세부사항은 인프라스트럭처에 위임한다. 이 계층은 업무용 소프트웨어의 핵심이다.    
인프라스트럭처 계층 상위 계층을 지원하는 일반화된 기술적 기능을 제공한다. 이러한 기능에는 애플리케이션에 대한 메시지 전송, 도메인 영속화, UI에 위젯을 그리는 것 등이 있다. 또한 인프라스트럭처 계층은 아키텍처 프레임워크를 통해 네 가지 계층에 대한 상호작용 패턴을 지원할 수도 있다.    
- 72p      

 

온라인 뱅킹 기능을 여러 계층으로 나누기

 

사용자 인터페이스 > 응용 > 도메인 > 인프라스트럭처


transferController / FundsTransferService / Account / Unit of Work Manager, O-R Mapper

- 73p 

 

하위 수준의 객체가 상위 수준의 객체와 소통해야 할 경우에는 (직접적인 질의에 응답하는 것 이상으로) 또 다른 메커니즘이 필요한데, 이 경우 콜백이나 관찰자 패턴처럼 계층 간에 관계를 맺어주는 아키텍처 패턴을 활용할 수 있다.

 

- 75p

 

 

눈여겨봐야 할 것은 응용 계층이 아닌 도메인 계층에서 주요 업무 규칙을 책임지고 있다는 것이며, 이 경우 "모든 대변"에는 그것과 일치하는 차변이 있다"가 업무 규칙에 해당한다. 

 

- 74p 

 

 

각 계층은 설계 의존성을 오직 한 방향으로만 둬서 느슨하게 결합된다. 상위 계층은 하위 계층의 공개 인터페이스를 호출하고 하위 계층에 대한 참조를 가지며(최소한 임시로라도), 그리고 일반적으로 관례적인 상호작용 수단을 이용해 하위 계층의 구성요소를 직접적으로 사용하거나 조작할 수 있다. 그러나 하위 수준의 객체가 상위 수준의 객체와 소통해야 할 경우에는(직접적인 질의에 응답하는 것 이상으로) 또 다른 메커니즘이 필요한데, 이 경우 콜백(callback)이나 OBSERVER(관찰자) 패턴처럼 계층 간에 관계를 맺어주는 아키텍처 패턴을 활용할 수 있다. 

 

-74~75p 

 

 

보통 인프라스트럭처 계층에서는 도메인 계층에서 어떤 활동이 일어나게 하지 않는다. 인프라스트럭처 계층은 도메인 계층의 "아래"에 있으므로 해당 인프라스트럭처 계층이 보조하는 도메인의 구체적인 지식을 가져서는 안 된다. 사실 그와 같은 기술적인 기능은 대개 SERVICE로 제공된다. 이를테면, 어떤 애플리케이션에서 이메일을 전송해야 한다면 메시지 전송 인터페이스가 인프라스트럭쳐 계층에 위치할 수 있으며, 애플리케이션 계층의 각 요소는 인프라스트럭처 계층에 메시지 전송을 요청할 수 있다. 

 

- 75p 

 

 

 

5장 소프트웨어에서 표현되는 모델 

 

 

어떤 객체가 연속성과 식별성을 지닌 것을 의미하는가? 아니면 다른 뭔가의 상태를 기술하는 속성에 불과한가? 이것은 ENTITY와 VALUE OBJECT를 구분하는 가장 기본적인 방법이다. 

 

- 84p 

 

 

객체 모델링을할 때 우리는 객체의 속성에 집중하곤 하는데, ENTITY의 근본적인 개념은 객체의 생명주기 내내 이어지는 추상적인 연속성이며, 그러한 추상적인 연속성은 여러 형태를 거쳐 전달된다는 것이다. 

 

- 93p 

 

 

뱅킹 애플리케이션에서 일어나는 거래를 생각해 보자. 같은 날 같은 계좌에 같은 금액을 예금하더라도 두 예금은 별개의 거래이므로 제각기 식별성을 지니고 ENTITY에 해당한다. 반면, 두 거래의 금액 속성은 아마도 동일한 금액 객체의 인스턴스일 것이다. 이러한 금액 속성은 서로 구별할 필요가 없으므로 식별성이 없다. 사실 두 객체는 속성이 동일하거나 심지어 반드시 같은 클래스가 아니더라도 서로 같은 식별성을 지닐 수도 있다. 

 

- 94p 

 

 

한 객체가 속성보다는 식별성으로 구분될 경우 모델 내에서 이를 해당 객체의 주된 정의로 삼아라. 클래스 정의를 단순하게 하고 생명주기의 연속성과 식별성에 집중하라. 객체의 형태나 이력에 관계없이 각 객체를 구별하는 수단을 정의하라. 객체의 속성으로 객체의 일치 여부를 판단하는 요구사항에 주의하라. 각 객체에 대해 유일한 결과를 반환하는 연산을 정의하라. 이러한 연산은 객체에 유일함을 보장받는 기호를 덧붙여서 정의할 수 있을지도 모른다. 이 같은 식별 수단은 외부에서 가져오거나 시스템에서 자체적으로 만들어 내는 임의의 식별자일 수도 있지만, 모델에서 식별성을 구분하는 방법과 일치해야 한다. 모델은 동일하다는 것이 무슨 의미인지 정의해야 한다.

 

- 94p 

 

 

 

소프트웨어 설계는 복잡성과의 끊임없는 전투다. 그러므로 우리는 특별하게 다뤄야 할 부분과 그렇지 않은 부분을 구분해야 한다. 

 

- 100p 

 

개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 VALUE OBJECT라고 한다. VALUE OBJECT는 설계 요소를 표현할 목적으로 인스턴스화되는데, 우리는 이러한 설계 요소가 어느 것인지에 대해서는 관심이 없고 오직 해당 요소가 무엇인지에 대해서만관심이 있다.

- 100p 

 

 

 

모델에 포함된 어떤 요소의 속성에만 관심이 있다면 그것을 VALUE OBJECT로 분류하라. VALUE OBJECT에서 해당 VALUE OBJECT가 전하는 속성의 의미를 표현하게 하고 관련 기능을 부여하라. 또한 VALUE OBJECT는 불변적으로 다뤄라. VALUE OBJECT는 아무런 식별성도 부여하지 말고 ENTITY를 유지하는 데 필요한 설계상의 복잡성을 피하라. 

- 101p 

 

 

 

자신의 본거지를 ENTITY나 VALUE OBJECT에서 찾지 못하는 중요한 도메인 연산이 있다. 이들 중 일부는 본질적으로 사물이 아닌 활동(activity)이나 행동(action)인데, 우리의 모델링 패러다임이 객체이므로 그러한 연산도 객체와 잘 어울리게끔 노력해야 한다.

-107p 

 

 

 

서비스라는 이름은 다른 객체와의 관계를 강조한다. ENTITY나 VALUE OBJECT와 달리 SERVICE를 정의하는 기준은 순전히 클라이언트에 무엇을 제공할 수 있느냐에 있다. ENTITY가 주로 동사나 명사로 이름을 부여하는 것과 달리 SERVICE는 주로 활동으로 이름을 짓는다. 또한 SERVICE도 추상적이고 의도적인 정의를 가질 수 있으며, 이것은 객체 정의와는 특성이 다르다. 아울러 SERVICE에도 마찬가지로 규정된 책임이 있을 것이며, SERVICE의 책임과 해당 책임을 이행하는 인터페이스는 도메인 모델의 일부로 정의될 것이다. 

- 108p 

 

 

 

SERVICE는 상태를 갖지 않게 만들어라. 

- 109p 

 

 

서비스를 여러 계층으로 분할하기 -> 응용, 도메인, 인프라스트럭처 

응용: 자금이체 응용 서비스 

도메인: 자금 이체 도메인 서비스

인프라스트럭처: 통지 서비스 

- 111p 

 

 

 

 

6장 도메인 객체의 생명주기 

 

AGGREGATE를 모델링하고 설계에 FACTORY와 REPOSITORY를 추가하면 모델 객체의 생명주기 동안 그것들을 체계적이고 의미 있는 단위로 조작할 수 있다. AGGREGATE는 생명주기의 전 단계에서 불변식이 유지돼야 할 범위를 표시해준다. 그리고 FACTORY와 REPOSITORY는 AGGREAGTE를 대상으로 연산을 수행하며 특정 생명주기로 옮겨가는 데 따르는 복잡성을 캡슐화한다. 

- 128p 

 

 

모델 내의 참조에 대한 캡슐화를 추상화할 필요가 있다. AGGREGATE는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음을 말한다. 각 AGGREGATE에는 루트와 경계가 있다. 경계는 AGGRGATE에 무엇이 포함되고 포함되지 않는지를 정의한다. 루트는 단 하나만 존재하며, AGGRGATE에 포함된 특정 ENTITY를 가리킨다. 경계 안의 객체는 서로 참조할 수 있지만 경계 바깥의 객체는 해당 AGGRGATE의 구성요소 가운데 루트만 참조할 수 있다. 루트 이외의 ENTITY는 지역 식별성을 지니며, 지역 식별성은 AGGREGATE내에서만 구분되면 된다. 이는 해당 AGGREGATE의 경계 박에 위치한 객체는 루트 ENTITY말고는 AGGREGATE의 내부를 볼 수 없기 때문이다.

-131p 

 

 

 

car, wheel, position, tire ... 

- 132p 

 

 

개념적 AGGREGATE를 구현하려면 모든 트랜잭션에 적용되는 다음과 같은 규칙이 필요하다. 

 

- 루트 ENTITY는 전역 식별성을 지니며 궁극적으로 불변식을 검사할 책임이 있다. 

- 각 루트 ENTITY는 전역 식별성을 지닌다. 경계 안의 ENTITY는 지역 식별성을 지니며, 이러한 지역 식별성은 해당 AGGREGATE 안에서만 유일하다.

- AGGREGATE의 경계 밖에서는 루트 ENTITY를 제외한 AGGREGATE 내부의 구성요소를 참조할 수 없다. 루트 ENTITY가 내부 ENTITY에 대한 참조를 다른 객체에 전달해 줄수는 있지만 그러한 객체는 전달받은 참조를 일시적으로만 사용할 수 있고, 참조를 계속 보유하고 있을 수는 없다. 루트는 VALUE OBJECT의 복사본을 다른 객체에 전달해 줄 수 있으며, 복사본에서는 어떤 일이 일어나든 문제되지 않는다. 이것은 복사본이 단순한 VALUE에 불과하며 AGGREGATE와는 더는 연관관계를 맺지 않을 것이기 때문이다. 

- 지금까지의 규칙을 바탕으로 결론을 내려보면 데이터베이스 질의를 이용하면 AGGREGATE의 루트만 직접적으로 획득할 수 있는 다른 객체는 모두 AGGREGATE를 탐색해서 발견해야 한다. 

- AGGREGATE안의 객체는 다른 AGGREGATE의 루트만 참조할 수 있다.

- 삭제 연산은 AGGREGATE 경계 안의 모든 요소를 한 번에 제거해야 한다.

- AGGREGATE 경계 안의 어떤 객체를 변경하더라도 전체 AGGREGATE의 불변식은 모두 지켜져야 한다.

 

- 133p 

 

 

 

어떤 객체나 전체 AGGREGATE를 생성하는 일이 복잡해지거나 내부 구조를 너무 많이 드러내는 경우 FACTORY가 캡슐화를 제공해준다.

-140p 

 

 

 

어떤 객체를 생성하는 것이 그 자체로도 주요한 연산이 될 수 있지만 복잡한 조립 연산은 생성된 객체의 책임으로 어울리지 않는다. 이런 책임을 클라이언트에 두면 이해하기 힘든 볼품없는 설계가 만들어질 수 있다. 클라이언트에서 직접 필요로 하는 객체를 생성하면 클라이언트 설계가 지저분해지고 조립되는 객체나 AGGREGATE의 캡슐화를 위반하며, 클라이언트와 생성된 객체의 구현이 지나치게 결합된다.

- 141p 

 

 

 

FACTORY를 잘 설계하기 위한 두 가지 기본 요건은 다음과 같다. 

 

1. 각 생성 방법은 원자적이어야 하며, 생성된 객체나 AGGREGATE의 불변식을 모두 지켜야 한다. 

2. FACTORY는 생성된 클래스보다는 생성하고자 하는 타입으로 추상화돼야 한다. 

-143p 

 

 

 

타협점을 고려해봤을 때 다음과 같은 상황에서는 공개 생성자를 사용하는 편이 좋다. 

- 클래스가 타입인 경우. 클래스가 어떤 계층구조의 일부를 구성하지 않으며, 인터페이스를 구현하는 식으로 다형적으로 사용되지 않는 경우

- 클라이언트가 STRATEGY를 선택하는 한 방법으로서 구현체에 관심이 있는 경우

- 클라이언트가 객체의 속성을 모두 이용할 수 있어서 클라이언트에게 노출된 생성자 내에서 객체 생성이 중첩되지 않는 경우

- 생성자가 복잡하지 않은 경우

- 공개 생성자가 FACTORY와 동일한 규칙을 반드시 준수해야 하는 경우. 이때 해당 규칙은 생성된 객체의 모든 불변식을 충족하는 원자적인 연산이어야 한다. 

 

- 146p 

 

 

 

 

REPOSITORY에는 다음과 같은 이점이 있다. 

- REPOSITORY는 영속화된 객체를 획득하고 해당 객체의 생명주기를 관리하기 위한 단순한 모델을 클라이언트에게 제시한다.

- REPOSITORY는 영속화 기술과 다수의 데이터베이스 전략, 또는 심지어 다수의 데이터 소스로부터 애플리케이션과 도메인 설계를 분리해준다.

- REPOSITORY는 객체 접근에 관한 설계 결정을 전해준다.

- REPOSITORY를 이용하면 테스트에서 사용할 가짜 구현을 손쉽게 대체할 수 있다. 

 

- 157p 

 

 

 

9장 암시적인 개념을 명확하게 

 

 

특별한 목적을 위해 술어와 유사한 명시적인 VALUE OBJECT를 만들어라. SPECIFICATION은 어떤 객체가 특정 기준을 만족하는지 판단하는 술어다.

-240p 

 

 

class DelinquentInvoiceSpecification extends InvoiceSpecification { 
	private Date currentDate;
    //	인스턴스는 한 날짜를 대상으로 사용된 후 폐기된다. 
    
    public DelinquentInvoiceSpecification(Date currentDate){
    	this.currentDate = currentDate;
    }
    
    public boolean isStatisfiedBy(Invocie candidate) { 
    	int gracePeriod = candidate.customer().getPaymentGracePeriod();
        Date firmDeadline = DateUtility.addDaysToDate(candidate.dueDate(), gracePeriod);
        return currentDate.after(firmDeadline);
    }
    
 }

 

 

 

 

- 243p 

 

 

 

 

 

10장 유연한 설계 

 

 

개발자가 컴포넌트를 사용하기 위해 컴포넌트의 구현 세부사항을 고려해야 한다면 캡슐화의 가치는 사라진다. 

- 261p 

 

 

수행 방법에 관해서는 언급하지 말고 결과와 목적만을 표현하도록 클래스와 연산의 이름을 부여하라. 이렇게 하면 클라이언트 개발자가 내부를 이해해야 할 필요성이 줄어즌다. 이름은 팀원들이 그 의미를 쉽게 추측할 수 있게 UBIQUITOUS LANGUAGE에 포함된 용어를 따라야 한다. 클라이언트 개발자의 관점에서 생각하기 위해 클래스와 연산을 추가하기 전에 행위에 대한 테스트를 먼저 작성하라.

 

- 262p 

 

방법이 아닌 의도를 표현하는 추상적인 인터페이스 뒤로 모든 까다로운 메커니즘을 캡슐화해야 한다.

-262p 

 

 

paint 메서드가 수행하는 작업을 짐작하는 유일한 방법은 코드를 읽는 것뿐이다.

 

public void paint(Paint paint){
	v = v + paint.getV();
 }

 

- 263p 

 

 

 

부수효과가 없는 함수 

- 266p 

 

 

 

 

 

public class PigmentColor {
	public PigmentColor mixedWith(PigmentColor other, double ratio) { 
    	 // 새로운 빨강, 파랑, 노랑 값을 할당하는
         // 많은 양의 복잡한 색상 혼합 코드가 이어짐
    }
}

public class Paint{
	public void mixIn(Paint other){ 
    	volume = volume + other.getVolume();
        double ratio = other.getVolume() / volume;
        pigmentColor = pigmentColor.mixedWith(other.pigmentColor(), ratio);
    }
}

 

 

새로운 Pigment Color 클래스는 도메인 내의 지식을 표현하고 해당 지식을 명확하게 전달한다. 또한 결과를 쉽게 이해할 수 있고 테스트하기가 용이하며 다른 연산과 결합하거나 단독으로 사용할 때 안전성을 보장받을 수 있는 SIDE-EFFECT-FREE-FUNCTION을 제공한다. 

 

- 271p 

 

 

 

개념적으로 의미 있는 기능의 단위를 찾게 되면 그 결과로 만들어진 설계는 유연하고 이해하기가 쉬워진다. 예를 들어 두 객체의 "합"이 도메인에서 의미를 가진다면 그 수준에서 메서드를 구현한다. add()를 두 개의 개별적인 단계로 나눠서는 안 된다. 동일한 연산 내에서 현재 다루고 있는 "합"의 의미를 넘는 수준까지 처리하려고 해서는 안된다. 규모가 좀 더 커진다면 각 객체를 하나의 완전한 개념인 "WHOLE VALUE"로 만들어야 할 것이다.

 

- 278p 

 

 

 

도메인을 중요 영역을 나누는 것과 관련한 직관을 감안해서 설계 요소(연산, 인터페이스, 클래스, AGGREGATE)를 응집력 있는 단위로 분해하라. 계속적인 리팩터링을 토대로 변경되는 부분과 변경되지 않는 부분을 나누는 중심 축을 식별하고, 변경을 분리하기 위한 패턴을 명확하게 표현하는 CONCEPTUAL CONTOUR를 찾아라. 우선적으로 확실한 지식 영역을 구성하는 도메인의 일관성 있는 측면과 모델을 조화시켜라. 

 

- 279p 

 

 

 

낮은 결합도는 객체 설계의 기본 원리다. 가능한 한 늘 결합도를 낮추고자 노력하라. 현재 상황과 무관한 모든 개념을 제거하라. 

- 284p 

 

 

 

 

정제된 설계에서 흔히 볼 수 있는 일반적인 실천지침으로 "CLOSURE OF OPERATION"이 있다. 이 명칭은 가장 정교한 개념체계인 수학에서 유래한 것이다. 1 + 1 = 2과 같은 덧셈 연산은 실수 집합에 대해 닫혀 있다.

 

- 286p 

 

 

적절한 위치에 반환 타입과 인자 타입이 동일한 연산을 정의하라. 구현자가 연산에 사용되는 상태를 포함하고 있다면 연산의 인자로 구현자를 사용하는 것이 효과적이므로 인자의 타입과 반환 타입을 구현자의 타입과 동일하게 정의한다. 이런 방식으로 정의된 연산은 해당 타입의 인스턴스 집합에 닫혀 있다. 닫힌 연산은 부차적인 개념을 사용하지 않고도 고수준의 인터페이스를 제공한다.

- 286p 

 

 

 

 

논리 연산을 이용한 SPECIFICATION 조합 

 

SPECIFICATION을 사용하다 보면 이내 여러 개의 SPECIFICATION을 조합해서 사용하면 유용한 상황에 마주치게 된다. 바로 위에서 설명한 것처럼 SPECIFICATION은 술어의 한 예이며, 술어는 "AND", "OR", "NOT" 연산을 사용해 조합할 수 있다. 이러한 논리 연산은 술어에 대해 닫혀 있어서 SPECIFICATION의 조합은 CLOSURE OF OPERATION을 의미한다. 

 

SPECIFICATION은 상당히 일반화된 기능을 지니고 있으므로 다양한 종류의 SPECIFICATION에 사용할 수 있는 추상 클래스나 인터페이스를 만드는 것이 여러모로 유용하다. 이것은 인자의 타입으로 고수준의 추상 클래스를 사용한다는 것을 의미한다. 

 

public interface Specification {
	boolean isSatisfiedBy(Object candidate);
}

 

 

이러한 추상화를 적용할 경우 메서드를 시작할 때 보호절이 필요하지만 기능 자체에는 영향을 미치지 않는다. 예를 들어 Container Specification은 다음과 같이 바뀐다. 

 

Public class ContainerSpecification implements Specification {
	private ContainerFeature reuqiredFeature;
    
    public ContainerSpecification(ContainerFeature required){
    	requiredFeature = required;
    }
    
    boolean isSatisfiedBy(Object candidate){
    	if (!candidate instanceof Container) return false;
        
        return (Container) candidate.getFeatures().contains(requiredFeature);
    }
 }

 

 

이제 세 가지 새로운 연산을 추가해서 Specification 인터페이스를 확장해보자. 

 

public interface Speicification {
	boolean isSatisfiedBy(Object candidate){
    
    Speicification and(Speicification other);
    Speicification or(Speicification other);
    Speicification not();
}

 

                

일부는 통풍 컨테이너를, 또 다른 일부는 강화 컨테이너를 요구하도록 Container Speicification을 설정했던 것을 떠올려보자. 휘발성인 동시에 폭발성이 강한 화학 물질인 경우에는 이 두가지 Speicification이 모두 필요할 것이다. 새로 정의한 메서드를 이용하면 이를 간단하게 처리할 수 있다. 

 

Speicification ventilated = new ContainerSpeicification(VENTILATED);
Speicification armored = new ContainerSpeicification(ARMORED);

Speicification both = ventilated.and(armored);

 

 

... 

 

COMPOSITE 패턴을 이용한 Speicification 설계 

 

public abstract class AbstracSpeicification implements Speicification {
	public Speicification and(Speicification other) { 
    	return new AndSpeicification(this, other);
    }
    
    public Speicification or(Speicification other){
    	return new OrSpeicification(this, other);
    }
    
    public Speicification not(){
    	return new NotSpeicification(this);
    }
}

public class AndSpeicification extends AbstractSpeicification { 
	Speicification one;
    Speicification other; 
    
    public AndSpeicification(Speicification x, Speicification y){
     	one = x;
        other = y; 
    }
    
    public boolean isSatisfiedBy(Object candidate){ 
    	return one.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate); 
    }
}


public class OrSpeicification extends AbstractSpeicification {
	...
    public boolean isSatisfiedBy(Object candidate){ 
    	return one.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate); 
    }
}


public class NotSpeicification extends AbstractSpeicification {
	Speicification wrapped;
    
       public boolean isSatisfiedBy(Object candidate){ 
    	return  !wrapped.isSatisfiedBy(candidate);
       }
}

 

 

 

- 293-296 p 

 

 

 

 

포섭 관계 

 

public class MinimunAgeSpecification {
	int threshold;
    
    public boolean isSatisfiedBy(Person candidate){
    	return candidate.getAge() >= threshold;
    }
    
    public boolean subsumes(MinimunAgeSpecification other){
    	return threshold >= other.getThreshold();
    }
}

 

 

- 300p 

 

 

 

 

12장 모델과 디자인 패턴의 연결 

 

프로세스에서 변화하는 부분을 별도의 전략 객체로 분리해서 모델에 표현하라. 프로세스의 규칙과 프로세스를 제어하는 행위를 서로 분리하라. STRATEGY 디자인 패턴에 따라 규칙이나 대체 가능한 프로세스를 구현하라. 다양한 방식으로 변형된 전략 객체는 프로세스의 서로 다른 처리 방식을 표현한다. 

- 332p 

 

 

 

중첩돼 있는 복합 객체 간의 관련성을 모델에 반영하지 않을 경우 계층구조상의각 수준에 공통적인 행위를 중복시킬 수밖에 없으며 복합 객체 내에 객체들을 중첩할 수 있는 유연성이 손상된다.(예를 들면, 복합 객체는 동일한 수준에 위치한 다른 복합 객체를 내부에 중첩할 수 없으며 중첩할 수 있는 수준의 수는 고정적이다). 계충구조상의 각 수준에서 다루는 개념에 차이가 없더라도 클라이언트는 서로 다른 수준을 처리하기 위해 각기 다른 인터페이스를 사용해야 한다. 집계 정보(aggregate information)를 산출하고자 계층구조를 재귀적으로 탐색하는 작업은 매우 복잡하다. 

... 

COMPOSITE 내부에 포함된 모든 구성요소를 포괄하는 추상 타입을 정의하라. 컨테이너에 포함된 항목의 집계 정보를 반환할 수 있게 정보를 제공하는 메서드를 컨테이너에 구현하라. "단말" 노드의 경우 자신의 값을 기반으로 정보를 제공하는 메서드를 구현하라. 클라이언트는 추상 타입만을 사용하므로 컨테이너와 단말 노드를 구분하지 않아도 된다. 

- 337p 

 

 

모델이 적용되는 컨텍스트를 명시적으로 정의하라. 컨텍스트의 경계를 팀 조직, 애플리케이션의 특정 부분에서의 사용법, 코드 기반이나 데이터베이스 스키마와 같은 물리적인 형태의 관점에서 명시적으로 설정하라. 이 경계 내에서는 모델을 엄격하게 일관된 상태로 유지하고 경계 바깥의 이슈 때문에 초점이 흐려지거나 혼란스러워져서는 안 된다.

- 362p 

 

 

 

프로젝트 상의 유요한 모델을 식별하고 각 BOUNDED CONTEXT를 정의하라. 여기에는 비객체지향적인 하위 시스템에 대한 암시적인 모델도 포함된다. 각 BOUNDED CONTEXT에 이름을 부여하고 이 이름을 UBIQUITOUS LANGUAGE의 일부로 포함시켜라. 

 

의사소통을 위해 컨텍스트 간의 번역에 대한 윤곽을 명확하게 표현하고 컨텍스트 간에 공유해야 하는 정보를 강조함으로써 모델과 모델이 만나는 경계 지점을 서술하라.

 

각 컨텍스트의 현재 영역을 나타내는 지도를 작성하라. 컨텍스트의 배치를 바꾸는 일은 나중에 하라.

 

- 371p 

 

 

 

FACADE는 하위 시스템에 대한 클라이언트의 접근을 단순화하고 더 쉽게 하위 시스템을 사용할 수 있게 만들어주는 대안 인터페이스에 해당한다. 우리는 사용하고자 하는 다른 시스템의 기능을 정확히 숙지하고 있으므로 이러한 기능에 접근하는 것을 촉진하고 능률화하며, 그 밖의 것은 감추는 FACADE를 만들어낼 수 있다. FACADE는 기저 시스템의 모델을 변경하지 않는다. FACADE는 다른 시스템의 모델에 따라 엄격하게 작성해야 한다. 그렇지 않으면 기껏해야 번역 책임이 다양한 객체로 확산되어 FACADE에 부담이 생길 것이고, 최악의 경우 다른 시스템이나 여러분만의 BOUNDED CONTEXT에 속하지 않는 또 다른 모델이 만들어질 것이다. FACADE는 다른 시스템의 BOUNDED CONTEXT에 속한다. 이러한 FACADE는 여러분의 요구에 맞게 특화된 더욱 친근한 외양을 제공할 뿐이다. 

 

ADAPTER는 행위를 구현하는 측에서 이해한 것과 다른 프로토콜을 클라이언트에게 사용하게 해주는 래퍼에 해당한다. 클라이언트에서 ADAPTER에 메시지를 전송하면 메시지는 의미상 동등한 메시지로 변환되어 어댑티(adaptee)에 전송된다. 즉, 응답이 변환되어 재전송 되는 것이다. 나는 "어댑터"라는 용어를 약간 포괄적으로 사용하고 있는데, 디자인 패턴에서는 클라이언트에서 예상하는 표준 인터페이스를 준수하는 래핑된 객체를 제작하는 데 주력하지만 여기서는 인터페이스를 적절히 개조해서 사용하기로 했으며, 어댑티는 객체가 아닐 수도 있기 때문이다. 여기서는 두 모델의 번역에 중점을 두지만 이 또한 어댑터의 목표에 부함한다고 본다. 

 

- 394p 

 

 

 

레거시 시스템의 단계적 폐기 

1. 단일 반복주기 내에서 신규 시스템에 추가될 수도 있는 레거시의 특정 기능을 파악한다.

2. ANTICORRUPTION LAYER에 필요할 추가사항을 파악한다.

3. 구현

4. 배치

5. ANTICORRUPTION LAYER에서 불필요한 부분을 파악해 이를 제거한다.

6. 지금은 사용되지 않는 레거시 시스템 모듈을 삭제하는 것을 고려해 본다. 

- 424p 

 

 

 

 

15장 디스틸레이션 

 

디스틸레이션은 혼합된 요소를 분리해서 본질을 좀더 값지고 유용한 형태로 뽑아내는 과정이다.

- 427p 

 

 

 

모델을 요약하라. CORE DOMAIN을 찾아 그것을 지원하는 다수의 모델과 코드로부터 쉽게 구별할 수 있는 수단을 제공하라. 가장 가치 있고 전문화된 개념을 부각시켜라. CORE를 작게 만들어라. 

- 432p 

 

 

 

현재 진행 중인 프로젝트를 위한 것이 아닌 응집력 있는 하위 도메인을 식별하라. 이러한 하위 도메인에서 일반화된 모델 요소를 추출해서 별도 MODULE에 배치하라. 해당 MODULE에는 여러분이 지닌 전문성의 자취를 남기지 않는다. 

- 436p 

 

 

보조적인 역할(잘못 정의된 것을 비롯해)로부터 CORE의 개념을 분리되게끔 모델을 리팩터링하고 CORE와 다른 코드와의 결합은 줄이면서 CORE의 응집력은 강화하라. 모든 일반적이거나 보조적인 역할을 하는 구성요소를 다른 객체로 추출해서 다른 패키지에 배치하라. 심지어 이러한 과정이 매우 긴밀하게 결합돼 있는 요소를 분리하는 식으로 모델을 피랙터링하는 것을 의미하더라도 말이다.

- 458p 

 

 

모델의 가장 근본적인 개념을 식별해서 그것을 별도의 클래스나 추상 클래스, 또는 인터페이스로 추출하라. 이 추상 모델이 중요 컴포넌트 간에 발생하는 상호작용을 대부분 표현할 수 있게끔 설계하라. 특화되고 세부적인 구현 클래스는 하위 도메인을 기준으로 정의된 자체적인 MODULE에 남겨둔 상태에서 이 추상적이면서 전체적인 모델을 자체적인 MODULE에 배치하라.

- 468p 

 

 

전부 리팩터링할 수도 없고, 고통 주도적일 수도 없다면 어떻게 해야할까? 

1. 고통 주도적 리팩터링에서는 문제의 근원에 CORE DOMAIN이나, CORE와 지원 요소와의 관계가 관련돼 있는지 살핀다. 만약 그렇다면, 이를 악물고 그 부분을 가장 먼저 고쳐야 한다. 

 

2. 마음껏 리팩터링할 수 있는 상황이라면 제일 먼저 CORE DOMAIN을 더 잘 분해하고, CORE의 격리를 개선하며, 보조적인 하위 도메인이 GENERIC하게 만드는 데 집중한다. 

 

이것이 바로 리팩터링에 들인 노력으로부터 최고의 가치를 얻는 방법이다. 

 

- 470p 

 

 

 

 

반응형