시스템이 너무 복잡해서 개별 객체 수준에서 완벽히 파악되지 않을 경우 커다란 모델을 솜씨있게 다루고 이해하기 위한 기법이 필요하다. 보통 큰 시스템의 목표는 전체 업무 도메인을 아우르는 긴밀히 통합된 시스템이다. 그러나 이런 전체 모델은 규모가 크고 복잡하여 하나의 단위로 관리하거나 이해하는 것이 불가능하다. 특히 어려운 것은 긴밀히 통합하는 것에서 오는 이점을 잃어버리지 않으면서도 모듈화를 달성하여 갖가지 하위 모듈이나 시스템들이 다양한 업무 영역을 다루기 위해 자연스럽게 상호작용하게끔 만드는 것이다.
전체를 포괄하는 모놀리식 형태의 도메인 모델은 비대해서 다루기 힘들고, 미묘한 중복이나 모순된 부분이 있을 것이다. 규모가 작은, 역할이 뚜렷이 구분되는 하위 시스템들이 임시방편으로 구현된 인터페이스를 토대로 서로 상호작용하도록 통합된다면 통합이 이루어지는 모든 곳에서 모델의 일관성 문제가 발생할 것이다. 이런 대규모의 시스템에서도 도메인 주도 설계는 구현과 동떨어지는 모델을 만들어 내지 않는다.
전략적 설계 원칙은 설계 의사결정이 중요한 상호운용성과 상승효과를 잃지 않으면서 각 하위 부분 간의 상호의존성을 줄이고 명확성을 향상시키게끔 이끌어야 한다. 그러면서 모델에 초점을 맞추어 시스템의 개념적 핵심, 비전을 포착해야 한다.
성공적인 모델은 규모와 상관없이 모순되거나 정의가 겹치지 않고 처음부터 끝까지 논리적인 일관성을 지녀야 한다.
여러 팀이 동시에 일을 진행하는 프로젝트가 있을 때, 각 팀이 맡은 영역의 맥락에서 구현된 객체나 속성들이 전체 도메인 관점에서 보면 서로 충돌을 일으킬 때가 있다. 이 경우 각 팀의 도메인 모델의 무결성을 해치지 않는 방법은 각 팀의 도메인 모델이 적용되는 경계를 명확히 하는 것이다.
모델의 가장 근본적인 요구사항은, 모델은 내적으로 일관성을 유지해야 하고 모델의 용어는 언제나 의미가 동일해야하며 어떠한 모순된 규칙도 있어서는 안된다는 것이다.
용어가 모호하지 않고 모순되는 규칙이 없는 모델의 내적 일관성을 단일화라고 한다. 모델에 논리적인 일관성이 없다면 그 모델은 아무런 의미가 없다.
큰 규모의 비즈니스를 다루는 시스템이라면 전체 도메인을 아우르면서, 모순되거나 용어의 정의가 겹치지 않고 일관성이 있는 단일 모델이 이상적이겠지만 현실은 그렇지 않다. 서로 다른 팀이 협업하는 큰 규모의 프로젝트에서 모델의 단일화를 유지하는 것은 쉽지 않다. 또한 대규모 시스템에서는 도메인 모델을 완전히 단일화하는 것은 타당하지 않거나 비용 대비 효과적이지 않다.
거대한 시스템을 단일 모델로 유지하려는 노력은 여러 위험 요소로 물거품으로 끝날 수 있다.
모델의 분화는 기술적인 내용 뿐만 아니라 서로 다른 팀의 의사결정 방식과 우선순위로 인해 발생되기도 한다. 완전한 통합을 가로막는 기술적 요인이 없더라도 프로젝트에는 다수의 모델이 나타날 수 있다.
어떤 것을 단일화할지 어떤 것을 단일화해서는 안 될지를 인식하고, 서로 다른 하부 시스템에 적용되는 여러 도메인 모델 간의 경계와 관계를 설정하는 수단이 필요하다.
다음 그림처럼 Bouneded Context와 Shared Kernel부터 시작하여 Separate Ways이 이르기까지, 각 Context (도메인 모델이 일관성을 유지하는 범위)를 연결하기 위한 전략으로 나아가야 한다.
규모가 큰 프로젝트에서는 서로 다른 도메인 영역을 다루는 다수의 도메인 모델들이 사용되기 마련인데, 개별 도메인 모델을 기반으로 작성된 코드가 한 데 섞이면 많은 버그가 발생하고 신뢰성이 떨어지는, 이해하기 어려운 소프트웨어가 만들어진다. 그 뿐만 아니라 팀 끼리, 아니면 같은 팀원들끼리 의사소통이 혼란스러워질 수도 있다.
다수의 모델으로 인해 발생하는 문제를 해결하기 위해서는 소프트웨어 내의 제한된 부분으로 모델의 단일화를 유지할 수 있는 범위를 명확하게 정의해야 한다. 어느 모델의 단일화가 적용될 수 있는 범위를 Context라고 하는데, 이 범위의 경계 안에서 모델은 일관된 상태로 유지되어야 하며 경계 바깥의 무언가에 의해 초점이 흐려지거나 혼란스러워져서는 안된다.
모델의 적용 범위를 제한하는 Bounded Context를 통해, 모델의 일관성이 적용되어야 할 부분과 서로 다른 Context끼리 어떤 식으로 관련되는지 명확히 이해할 수 있도록 도와준다. Context 내에서는 도메인 모델이 논리적으로 일관성 있는 상태가 유지된다.
Bounded Context를 정의함으로써 얻을 수 있는 것은 이 범위내에서 모델을 일관성 있게 유지함으로써 나타나는 모델의 명확성이다. 모델이 적용되는 범위, Context를 명시적으로 정의하라. Context의 경계를 팀 조직, 애플리케이션의 특정 부분이나 코드, 데이터베이스 스키마와 같은 물리적인 형태의 관점에서 명시적으로 설정하라.
경계는 특별한 곳이다. 서로 다른 Bounded Context끼리 간의 관계에 대해서는 항상 주의가 필요하다. Context Map은 여러 Context 사이의 연결이라는 큰 그림을 제시하면서 각 Context가 차지 하는 영역을 보여준다. 그리고 일단 Context가 제한되면 지속적인 통합 (Continuous Integration) 프로세스를 토대로 Context 내부에 있는 모델의 단일화를 유지할 수 있다.
모델의 단일화가 깨진다면 어떤 모습을 띠게 될까? 여러 징후를 바탕으로 알 수 있는데, 그 중 하나는 바로 코드로 작성된 인터페이스가 서로 맞지 않는 경우이다. 또 중복된 개념이나 허위 동족 언어(false cognates)이 있다.
중복된 개념이란 실제로는 같은 개념을 나타내는 두 개의 모델 요소와 구현 코드가 존재하는 것이다. 이러면 요구사항이 변경될 때마다 두 군데 이상을 갱신하고 변환해야 한다.
허위 동족 언어는 같은 용어나 구현 객체를 사용하는 두 사람이 서로 같은 것을 이야기하고 있다고 생각하지만, 실제로는 그렇지 않은 경우를 말한다. 이러한 개념상의 충돌은 알아차리기 힘들 뿐만 아니라, 서로의 코드를 침범하게 되고 데이터베이스에 이상한 불일치가 생길 뿐만 아니라 팀 내 의사소통이 혼란스러워질 수 있다.
Bounded Context를 정의했다면 이를 건전한 상태로 유지해야 한다.
다수의 사람이 한 Bounded Context 내에서 작업할 경우, 모델이 단편화될 가능성이 있다. 팀의 규모와 상관없이 서너 명 정도에 달하는 소수의 인원이더라도 심각한 문제에 마주칠 수 있다. 이걸 피한답시고 너무 작은 Context로 쪼갠다면 가치 있는 수준의 통합과 응집성을 잃어버릴 수도 있다.
다른 사람이 모델링한 객체나 상호작용의 의도를 완전히 이해하지 못한 채 객체를 수정해서 원래의 목적으로 사용하지 못하거나, 다루고자 하는 개념이 이미 모델의 다른 부분에 있는데도 이 사실을 모른채 중복 구현할 수도 있다.
규모와는 상관없이 통합된 시스템을 개발하는 데, 최소한의 의사소통을 유지하기란 매우 어려운 일이다. 따라서 의사소통을 촉진하고 복잡도를 줄일 방법이 필요하다. 또한 기존 코드를 망가뜨릴지도 모른다는 두려움에 코드를 중복시키는 등의 행위를 방지할 안전망이 필요하다.
Bounded Context 안에 포함된 모델의 무결성, 단일화를 유지하기 위해 지속적인 통합 (Continuous Integration) 프로세스를 보유해야 한다. 지속적인 통합은 내부적으로 모델의 균열이 발생하였을 때 이를 빠르게 감지하고 수정할 수 있을 정도로 Context 내에 일어나는 모든 작업을 빈번히 병합하여 일관성을 유지하는 것을 말한다.
Context 내의 모든 작업을 병합하고 일관성있게 유지하는 지속적인 통합 프로세스를 통해 이러한 균열을 빠르게 정정해나가야 한다. 모델 개념은 팀원들 간의 부단한 의사소통을 토대로 통합하고, 끊임없이 변화하는 모델을 함께 이해하고 발전시켜야 한다. 가장 근본적인 것은 Ubiquitous Language를 다듬는 것이다. 그리고 구현 산출물은 모델 내의 균열을 방지하는 체계적인 병합 / 빌드 / 테스트 프로세스를 통해 통합하면 된다.
다음과 같은 개발 프로세스로 효과적으로 통합시킬 수 있다.
모델의 단편화가 발생했다는 사실을 빠르게 알려줄 수 있는 자동화된 테스트를 구축하고, 모든 코드와 그 밖의 구현 산출물을 빈번하게 병합하는 프로세스를 수립하라. 모델의 개념이 각 팀원의 머릿속에서 발전해감에 따라, 모델에 관한 각자의 시각 차이를 해소하기 위해 끊임없이 Ubiquitous Language를 사용하라.
개별적인 Bounded Context 만으로는 전체를 조망할 수 없다. 다른 팀에 속한 사람들은 Context 간의 경계를 인식하지 못할 것이며, 자신도 모르게 Context의 경계를 흐리게 하거나 연결되는 방식을 복잡하게 바꿀 것이다. 서로 다른 Context를 연결해야 하는 경우 서로 다른 Context는 상대에게 스며드는 경향이 있다.
Bounded Context 간에 코드를 재사용하는 것은 모델의 모호함을 초래할 수 있어 피해야 한다. 번역 과정을 통해 서로 다른 Context를 통합해야 하며, 각 Context 간의 관계를 정의하고 모든 모델 Context를 아우르는 전체적인 뷰, Context Map을 만들면 혼란을 줄일 수 있다.
Context Map은 프로젝트 관리와 소프트웨어 설계 영역 사이에 걸쳐 있는 개념이다. 대개 큰 프로젝트의에서 각 Context의 경계는 팀 조직이 어떻게 구성되어 있느냐에 따라 정해질 수 있다.
프로젝트 상의 유효한 모델을 식별하고 각 Bounded Context를 정의하라. 그리고 각 Bounded Context에 이름을 부여하고, 이 이름을 Ubiquitous Language의 일부로 포함시켜라. 의사소통을 위해 Context 간의 번역에 대한 윤곽을 명확히 표현하고, Context 끼리 공유해야 하는 정보를 강조하여 모델과 모델이 만나는 경계 지점을 서술하라.
서로 다른 Bounded Context 와 접촉하는 지점은 테스트할 때 특히 중요하다. 대게 테스트는 Context 경계에 존재하는 번역의 미묘한 차이와 낮은 수준의 의사소통을 보완하는 데 기여한다.
Context Map을 조직화, 문서화할 때는 아래의 두 가지 사항이 중요하다.
Bounded Context의 이름은 해당 Bounded Context에 관해 이야기할 수 있는 것이어야 한다. 그러한 이름은 팀의 Ubiquitous Language에 포함되어야 한다.
모든 이들이 경계가 어디에 위치하는지 알아야 하며, 어떠한 코드나 환경의 Context도 인식할 수 있어야 한다.
기능 통합에 한계가 있는 경우, Continuous Integration(지속적인 통합)에 따르는 비용이 너무 높다고 판단할 수 있다. 밀접하게 연관된 애플리케이션을 대상으로 작업 중인 팀 간의 협력이 조율되지 않는다면, 각 팀이 만들어낸 결과물을 함께 조합하기는 쉽지 않을 수 있다.
만약 서로 다른 Context를 통합하기에는 너무 많은 비용이 들고 의사소통의 품질을 유지하기가 쉽지 않다면 어느 특정 영역을 신중히 선택하여 각 팀이 공유하는 방법으로, 적은 비용으로 상당한 이익을 얻을 수도 있다. 만약 이런 방법이 다른 번역 패턴보다 이익이 더 크다면, 공유하기로 한 도메인 모델의 부분 집합을 명시하라. 물론 모델의 부분 집합뿐만 아니라 모델 요소와 관련된 코드나 데이터베이스 설계의 부분 집합까지도 포함된다. 이런 공유되는 영역은 다른 팀과의 협업없이는 함부로 변경해서는 안된다.
서로 다른 팀에서 모두 필요한 부분이면 어떤 부분이라도 Shared Kernel이 될 수 있으며, 이런 공유되는 영역인 Shared Kernel은 설계의 다른 부분처럼 쉽게 변경할 수는 없다. 양 팀에서 작성한 테스트 스위트를 통과해야할 뿐만 아니라, 팀 간의 Shared Kernel 영역에 대한 지속적인 통합 프로세스도 필요하다.
만약 Context들이 서로 다른 기술로 구현될 경우에는 Shared Kernel을 유지하기가 쉽지 않다.
종종 하위 시스템의 역할이 다른 시스템에서 필요한 데이터를 공급하는 구조로 보이는 경우가 있다. 예를 들면 사용자의 행동을 분석하여 데이터를 쌓아두는 시스템이 있는가하면, 그 데이터를 활용하여 또 다른 비즈니스 영역에 활용하는 시스템도 있을 것이다. 이러한 시스템 간의 의존성은 모두 단방향으로 흐른다. 이런 경우 수행하는 업무가 상이하고 서로 다른 모델을 사용하는 편이 나을 수 있다.
이런 단방향의 의존성을 가지는 시스템들은 자연스럽게 서로 다른 Bounded Context로 나뉜다. 특히 두 시스템을 개발함에 있어서 상이한 기술을 사용하면 더 그렇다.
번역 계층을 구현함에 있어서, 의존성이 한 방향으로 흐르게 구현하는 것이 더 쉽다.
이런 경우에는 팀 간의 명확한 고객 / 공급자 관계를 확립함으로써 서로 다른 Context끼리의 번역을 수월하게 진행할 수 있다.
현재 개발하고 있는 Context가 어느 특정 컴포넌트나 시스템에 의존하는 관계로, 의존성이 단방향으로 흐른다고 가정하자. 그런데 정작 의존하고 있는 컴포넌트나 시스템을 담당하는 팀이 자신의 요구사항에 무관심하거나 아니면 우선순위가 낮거나 협력이 원활하지 않는 경우가 있다.
만약, 서로 다른 하위 시스템을 구현하는 팀끼리 협업이 제대로 되지 않거나 이로 인해 번역 계층을 구현함에 있어서 어려움을 느낀다면 이에 대한 대안이 있어야 할 것이다. 이 대안에 대한 선택은 팀의 정책이나 필요로 하는 컴포넌트의 설계 품질과 스타일에 달려 있다.
만약 자신의 팀이 협력하고 있는 팀과 관계를 끊기로 했다면, Separate Ways로 나가는 것이다.
사용하는 컴포넌트나 하위 시스템의 가치가 매우 커서 계속 사용해야 되는 경우도 있다. 하지만 그 컴포넌트 / 시스템의 설계 품질이 떨어지고 그 시스템의 모델이 자신이 개발 중인 Context 내부의 모델과 동떨어져서 자체적인 독립 모델로 만들어야 한다면 어쩔수 없이 번역 계층을 개발하고 유지보수해야 하는 책임을 우리가 맡아야 한다.(Anticorruption Layer)
하지만 그렇게 품질이 떨어지지 않고 합리적으로 수용할 수 있다고 한다면, 전적으로 자체적인 독립 모델을 만드는 것을 포기하고 그 컴포넌트 / 시스템의 모델에 100% 따르는 방법이 있다.(Conformist) 이를 통해 Bounded Context 간의 번역에 따른 복잡도를 제거하고 통합 자체를 단순화시킬 수 있다. 하지만 설계 형식이, 의존하고 있는 컴포넌트를 개발하는 팀에 속박되고 이상적인 모델을 만들지 못할 수도 있다.
이런 의사결정은 특정 컴포넌트나 Context에 의존성을 심화시킨다. 이를 피하기 위해서는 Anticorruption Layer인 번역 계층을 통해 스스로 자신이 개발하는 Context를 보호해야 한다.
Shared Kernel과 Conformist는 다른 두 Context끼리 동일한 모델을 이용하는 영역이 있다는 점에서 유사하지만 결정적인 차이점은 의사결정과 개발 과정에 있다. Shared Kernel이 밀접하게 조율하는 두 팀간의 협력관계를 다룬다면, Conformist는 협력에 관심이 없는 팀과의 통합 문제를 다룬다.
대형 프로젝트에서 하나의 하위 시스템은 여러 독립적으로 개발되는 다른 하위 시스템과 상호작용해야 한다. 이러한 하위 시스템은 문제 도메인을 각기 다른 식으로 모델에 반영할 것이다. Context 끼리 간에는 서로 다른 도메인 모델이 있고, 서로 개념이 일치하지도 않을 것이다. 따라서 각 Context 사이에 특정 계층을 사이에 두어 모델의 일관성을 해지지 않도록 번역을 해주지 않는다면 하위 시스템끼리 통합에 있어서 오류를 일으키거나 데이터베이스가 손상될 수도 있다.
Anticorruption Layer는 개념적인 객체와 행위를 표현한 하나의 모델 요소들을 다른 모델 요소들로 변환하기 위한 매커니즘이 구현되는 곳이다. 이를 통해 Context 간, 각자의 모델 단일화를 유지할 수 있다.
이 계층에서의 공용 인터페이스는 Entity의 형태를 띠기도 하지만 보통 Service 집합으로 표현된다. 번역을 담당하는 새로운 계층을 구현하다보면 타 시스템의 행위를 새롭게 추상화할 수 있고, 다른 시스템의 서비스나 데이터를 자신이 개발하는 Context의 모델에 맞게 변환해서 제공할 수 있다. 모델 관점에서 응집력 있는 책임을 맡은 여러 개의 Service나 또는 Entity를 사용하도록 한다.
Anticorruption Layer는 두 Bounded Context를 잇는 수단이다.
여러 시스템 간의 상호작용에 필요한 통신 및 전송 메커니즘과 Facade, Adapter와 같은 패턴, Translater(번역기)를 조합하는 것이다.
Facade
한쪽 모델에서 다른 모델로 번역하는 것을 구현하다보면, 상호작용하기 어려운 여러 타 시스템의 인터페이스를 동시에 다루어야 하는데, 이를 Facade를 통해 쉽게 구현할 수 있다. Facade는 하위 시스템에 대한 클라이언트의 접근 방법을 단순화하고 더 쉽게 사용할 수 있도록 해주는 대안 인터페이스에 해당한다. 이를 통해 하위 시스템의 기능을 더 쉽게 사용할 수 있을 뿐만 아니라 필요없는 부분은 감추어 캡슐화할 수도 있다. 단, Facade는 다른 시스템의 모델에 따라 작성해야하며, 그렇지 않으면 번역 책임이 다양한 객체로 확산될 수 있다. Facade는 번역하고자 하는 다른 시스템의 Bounded Context에 속하는 것이다. 단지 우리의 요구에 맞게 특화된 더욱 친근한 외양을 제공할 뿐이다.
Adapter
Adapter는 행위를 구현한 측에서 사용한 것과 다른 프로토콜을 클라이언트에서 사용할 수 있도록 해주는 래퍼에 해당한다. 클라이언트에서 Adapter를 통해 메시지를 전송하면, 변환되어서 재전송한다. Anticorruption Layer에서 구현되는 Service는 인터페이스를 지원하고, 다른 시스템이나 Facade에 요청하는 방법을 알고 있는 Adapter가 필요하다.
Translator
Adapter가 하는 일은 요청 방법을 파악하는 것임에 비해, 객체나 데이터를 실제 변환하는 방법은 자체적인 객체에 두는 개별적이고 복잡한 작업이다. 번역기는 필요할 때 인스턴스화되는 경량 객체일 수도 있다. 번역기는 Adapter에 속하므로 아무런 상태도 필요하지 않으며 분산될 필요가 없다.
일반적으로 각 Bounded Context마다 통합해야 할 외부 Context의 구성요소에 대해 번역 계층을 정의할 것이다. 통합이 1회성으로 끝난다면 번역 계층을 삽입하는 것이 최소한의 비용으로 자신의 모델이 손상되는 것을 방지할 수 있다. 그런데 어느 특정 하위 시스템을 필요로 하는 곳이 많다면 좀 더 유연한 접근법이 필요하다.
시스템을 다른 여러 하위 시스템들과 통합하여 각 하위 시스템에 대한 번역 계층이 구현되어 있을 때, 요구사항 변경과 같은 이유로 코드 변경이 발생하면 여러 번역 계층을 유지보수해야 될 수 있다.
하지만 시스템이 일관성 있게 다른 하위 시스템들에게 기능을 제공할 수 있다면, 여러 하위 시스템의 공통적인 요구사항을 만족하는 일련의 Service 형태로 구현하는 것도 가능하다. 시스템 접근과 관련된 프로토콜을 일련의 Service로 정의하라. 프로토콜을 공개하여 시스템과 통합하고자 하는 모든 이들이 해당 프로토콜을 사용할 수 있도록 하라.
각기 다른 시스템들이 요구하는 공통의 요구사항을 처리하게끔 프로토콜을 개선하고 확장하도록 하고, 특수한 요구사항은 일회성 번역기로 프로토콜을 보강하도록 하여 공개된 프로토콜을 다른 하위 시스템 측면에서 봤을 때는 단순하고 일관성있도록 유지해야 한다.