도메인 주도 설계 - 14. 모델의 무결성 유지

dvmflstm·2020년 2월 24일
0
post-thumbnail

도메인 모델은 단일화(unification)을 유지해야 한다. 단일화란 모델에서 사용하는 각 용어가 모호하지 않고 모순되는 규칙이 없는 일관성 있는 상태를 뜻한다. 모델의 단일화는 소프트웨어 개발 과정에서 여러 요인에 의해 흔들릴 수 있다. 대규모 시스템을 개발할 때는 한 시스템 내에 다수의 모델이 존재할 수도 있고, 그 모델들이 서로 어느정도의 관계를 이루고 있다면 각각의 모델은 단일화를 유지하기 위해 노력을 기울여야 한다. 또한 시스템 내에 모델이 하나라고 해도 ubiquitous language가 흐트러지면 의사소통의 부재로 모델의 단일화가 흐트러질 수 있다. 이번 장에서는 모델을 무결성 있게 유지하기 위해 알아야 하는 개념들을 소개한다.

BOUNDED CONTEXT (제한된 컨텍스트)

규모가 큰 프로젝트에서는 다수의 모델이 사용되기 마련이다. 그러나 개별적인 모델을 기반으로 작성된 코드가 한데 섞이면 많은 버그가 발생하고 신뢰성이 떨어지며 이해하기 힘든 소프트웨어가 만들어진다.

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

BOUNDED CONTEXT VS MODULE

MODULE과 BOUNDED CONTEXT의 개념이 비슷하게 느껴질 수도 있는데, 내가 이해한 바로는 BOUNDED CONTEXT는 MODULE보다 훨씬 큰 개념으로 하나의 도메인 모델 전체를 감싸고 있는 사용자의 활동 맥락을 뜻한다고 생각했다. 반면 MODULE은 단일 컨텍스트 내에서 하위 도메인을 분리하기 위해서도 사용할 수 있고, 서로 다른 두 BOUNDED CONTEXT는 아무래도 당연히 다른 네임스페이스를 가지는 MODULE에 위치할 것이다.

CONTINUOUS INTEGRATION (지속적 통합)

앞서 말했듯이 모델은 단일화를 유지해야 한다. 모델 내의 각 요소들은 모순 없이 타 요소들과 유기적으로 소통해야 하는데, 컨텍스트의 규모가 커지면 모델이 단편화될 가능성이 높아진다. 그렇기 때문에 우리는 단편화가 발생했다는 사실을 빠르게 알려줄 수 있는 자동화된 테스트와 함께 모든 코드와 구현 산출물을 빈번하게 병합하는 프로세스를 수립해야 한다. 이를 CONTINUOUS INTEGRATION이라고 부르며, 최근에는 훌륭한 CI 도구들이 많이 나와 있다.

CI라는 개념을 나름 익숙하게 생각하고 있고, 개발 과정에서 적극적으로 도입해본 적도 있지만 CI라는 단어가 가지는 의미와 탄생 배경에 대해서는 알아본적이 없었다. DDD를 통해 CI가 필요한 자연스러운 배경을 알 수 있게 된 것 같다.

CONTEXT MAP

BOUNDED CONTEXT와 CONTINUOUS INTEGRATION으로 단일 모델의 단편화를 막고 단일화를 유지하는 방법을 알아보았다. 하지만 이것으로는 전체를 조망할 수 없다. 다른 모델의 컨텍스트는 여전히 유동적이고 모호하다.

프로젝트상의 유용한 모델을 식별하고 각 BOUNDED CONTEXT를 정의한 다음(BOUNDED CONTEXT의 이름은 컨텍스트 내 모델의 UBIQUITOUS LANGUAGE에 포함되어야 한다), 컨텍스트 간의 번역에 대한 윤곽을 명확하게 표현하고 컨텍스트 간에 공유해야 하는 정보를 강조함으로써 모델과 모델이 만나는 경계지점을 서술해야 한다. 이렇게 각 컨텍스트의 현재 영역을 나타내는 지도를 작성하자.

서로 다른 두 BOUNDED CONTEXT가 소통하기 위해서는 각각 독자적으로 발전해 온 UBIQUITOUS LANGUAGE 간의 번역 과정이 필요하다. 각 BOUNDED CONTEXT의 요소들이 어떻게 매핑되고 번역되는 지를 CONTEXT MAP을 통해 비교적 쉽게 확인할 수 있으며, 번역과정에서 10장에서 배운 ASSERTION이 큰 도움을 준다.

예제: 운송망 CONTEXT와 예약 CONTEXT간 요소 매핑

목적 : Routing Service에서 route specification을 인자로 받아 이 specification을 만족하는 itinerary를 반환하도록 하고 싶음. 그런데 운송망 context에서 차용할 만한 개념들이 존재하므로 이 컨텍스트와 소통하면서 이 기능을 구현하고 싶음.

요구사항은 Routing Service의 요청을 받아들여 Network Traversal Service(운송망 탐색 서비스)에서 이해할 수 있는 용어로 번역한 다음, 그 결과를 다시 Routing Service에서 받아들일 수 있는 itinerary(운항 일정)으로 번역하여 전달하는 것이다.

아래는 서로 다른 두 컨텍스트에서 매핑되어야 하는 개념을 나타낸 표이다.

| 예약 컨텍스트 | 운송망 컨텍스트 |
|:----------:|:------------:|
| Route specification | Node의 위치 코드 List |
| Itinerary | Node의 id List |

첫 번째 번역의 경우 Route specification에 담긴 출발지와 도착지를 list의 처음과 마지막에 배치하고, 중간 경로를 가운데에 넣는 방식으로 쉽게 번역이 가능하다.

두 번째 번역은 itinerary (운항 일정) 가 Leg (구간) 의 list로 이루어져 있으므로, 운송망 컨텍스트에서의 Node 두 개를 하나의 Leg로 묶어 하나는 Leg의 적재 위치, 하나는 Leg의 하역 위치로 두면 번역이 가능하다.

SHARED KERNEL

서로 다른 두 팀의 모델을 하나로 통합하기란 그 모델이 밀접하게 연관되어 있다고 하더라도 쉽지 않은 일이다. 팀간의 협력이 조율되지 않는다면 모델을 통합하고 CONTINUOUS INTEGRATION을 마련하는 데에 많은 시간을 허비하게 될 것이며, 명확하게 재정비되지 않은 UBIQUITOUS LANGUAGE는 그 가치를 잃어버릴 것이다. 그러므로 도메인 모델의 일부만을 공유함으로써 적은 비용으로 커다란 이익을 얻는 방법을 생각해볼 수 있다.

두 팀 간에 공유하기로 한 도메인 모델의 부분집합을 명시하라. 명시적으로 공유하는 부분들은 특별한 상태를 가지며, 다른 팀과의 협의 없이는 변경할 수 없다. 이 부분에 대한 CI는 양 팀에서 작성한 테스트를 모두 실행해야 한다.

CUSTOMER/SUPPLIER DEVELOPMENT TEAM (고객/공급자 개발 팀)

서로 다른 두 컨텍스트가 특별한 관계를 가지는 경우가 있다. 기업의 영업활동에는 상류 활동과 하류 활동이 있는데, 상류 활동이란 제품과 용역생산의 첫 단계에서의 활동을 지칭하며 하류 활동이란 소비자의 소비에 가까운 부분에서의 영업활동을 뜻한다. 소프트웨어 개발을 하다보면 상류 활동을 모델링할 수도, 하류 활동을 모델링할 수도 있다. 이 두 모델이 상호작용하는 양상은 조금 특별한데, 하류 활동은 상류 활동에 아무런 피드백을 제공하지 않으며 모든 의존성은 단방향으로, 상류에서 하류로 흐른다. 이러한 관계가 애플리케이션 내에 존재한다는 것을 파악하는 것이 중요하며, 이를 고려해 모델링 방법을 고민해야 한다.

두 팀 간에 고객/공급자 관계가 존재하는 지 확인하고, 존재한다면 명확히 확립하라. 계획 회의에서 하류 팀이 상류 팀에 대한 고객 역할을 맡게 하라. 하류 요구사항에 대한 작업을 협상하고 이에 대한 예싼을 책정해서 모든 이들이 일정과 약속을 이해할 수 있게 하라.

이렇게 고객/공급자 관계를 명확히 확립하고, 개발 및 설계 과정에 이를 적극적으로 반영한다면 상류 팀이 하류 팀을 망가뜨릴지도 모른다는 두려움 없이 코드를 수정할 수 있다. 하류 팀은 상류 팀을 지속적으로 감시하지 않고도 자신의 작업에 집중할 수 있게 자동화된 테스트 스위트를 마련해야 한다.

*(테스트 스위트 : 테스트 대상 컴포넌트나 시스템에 사용되는 여러 테스트 케이스의 집합. 테스트 스위트는 테스트 사후조건이 주로 다음 테스트를 위한 사전조건이 되는 테스트 케이스로 구성된다.)

CONFORMIST (준수자)

위에서 설명한 상류팀과 하류팀의 관계에서 두 팀이 활발히 상호작용한다면 좋겠지만 그렇지 못한 경우도 있다. 시장 방향성을 바꿈에 따라 SUPPLIER는 기존 CUSTOMER에게 투자할 가치를 못 느끼게 될 수도 있고, SUPPLIER의 운영 상태가 불안정해짐으로써 CUSTOMER는 SUPLLIER의 충분한 공급을 기대하지 못할 수도 있다.

이렇게 비관적인 상황에서 하류팀이 택할 수 있는 여지는 크게 세 가지 정도가 있다.

  • SEPERATE WAYS (아래에서 설명)
  • ANTICORRUPTION LAYER (아래에서 설명)
  • CONFORMIST

지금 설명하는 CONFORMIST 패턴은 하류팀이 전적으로 독립적인 모델을 포기하고 상류 팀의 모델을 완전히 수용하며 개발을 진행하는 것이다. COMFORMIST가 되기로 마음 먹었다면 맹목적으로 상류팀의 모델을 준수해서 BOUNDED CONTEXT 간의 번역에 따른 복잡도를 제거해야 한다. 이 경우 하류 팀 설계자들의 설계 형식이 상류 팀에 속박되고 애플리케이션을 위한 이상적인 모델을 만들지는 못해도 통합 자체는 매우 단순해질 수 있다.

CONFORMIST는 동일한 모델을 공유한다는 측면에서 위에서 설명한 SHARED KERNEL과 유사하다. 그러나 SHARED KERNEL은 밀접하게 조율하는 두 팀 간의 협력관계를 다루는 반면 CONFORMIST는 협력에 관심이 없는 팀과의 통합 문제를 다룬다.

ANTICORRUPTION LAYER (오류 방지 계층)

ANTICORRUPTION LAYER는 타 애플리케이션에는 협력이나 유용한 설계가 존재하지 않는다고 가정하는 협력관계를 다루는 패턴이다.

anticorruption layer는 크게 두 가지 요소로 구성 된다.

FACADE (파사드)

우리는 협력이나 유용한 설계가 존재하지 않는 하위 시스템과의 협력을 가정하고 있기 때문에, 이 하위 시스템은 복잡한 인터페이스를 가진 레거시 시스템일 가능성이 높다. 이 복잡한 인터페이스를 우리가 개발하고 있는 하위 시스템에 친숙한 형태의 대안 인터페이스로 바꿔주는 역할을 하는 요소가 바로 facade다. 주의할 것은 facade는 협력하려는 타 시스템의 모델 요소를 전혀 변경하지 않고, 우리에게 좀 더 친숙한 외양을 제공할 뿐이라는 것이다. 물론 타 시스템의 인터페이스가 그리 복잡하지 않다면 파사드는 존재하지 않아도 될 것이다.

ADAPTER (어댑터)

adapter는 위에서 설명한 translator(번역기)의 기능을 포함하는 요소로, 서로 다른 프로토콜 간의 변환을 맡게 된다. 개념 객체나 데이터의 변환 로직은 개별적이고 복잡한 작업이므로 번역 로직 자체가 adapter 내에 위치하지는 않을 것이다.

SEPERATE WAYS (각자의 길)

한 시스템 내의 서로 다른 두 모델이 의사소통하는 부분이 거의 없고, 통합을 통해 얻을 수 있는 이득이 크지 않다면 그냥 두 모델은 의사소통하지 않고 독립적으로 두는 것이 좋은 선택일 수 있다.

이러한 상황에서는 BOUNDED CONTEXT가 다른 것과 아무런 관계도 맺지 않도록 선언해서 개발자들이 이 작은 범위 내에서 단순하고 특화된 해결책을 찾을 수 잇게 해야 한다.

OPEN HOST SERVICE

어떤 하위 시스템은 태생적으로 타 시스템과 의사소통하는 부분이 많을 수 밖에 없는 경우도 있다. 이러한 경우 관계를 맺는 타 시스템마다 모두 번역기를 두어 조정한다면 관리 포인트가 너무 많이 늘어날 것이다. 이러한 상황에서는 보편적으로 외부에 공개할 만한 서비스들을 모아 OPEN HOST SERVICE로 만드는 방안을 생각해볼 수 있다.

하위 시스템 접근과 관련된 프로토콜을 일련의 SERVICE로 정의한다. 프로토콜을 공개해서 개발 중인 시스템과 통합하고자 하는 모든 이들이 해당 프로토콜을 사용할 수 있게 하자. 물론 특수한 요청의 경우 일회성 번역기로 프로토콜을 보강해야 할 것이다.

profile
서울대학교 컴퓨터공학부 github.com/BaekGeunYoung

0개의 댓글