도메인 주도 설계 첫걸음 책을 읽고 정리한 내용입니다.
1. 비즈니스 도메인 분석하기
조직의 비즈니스 전략과 소프트웨어를 만들면서 얻고자 하는 가치를 이해해야 한다.
비즈니스 도메인이란?
회사가 고객에게 제공하는 서비스를 말한다. 예를 들면 스타벅스는 커피로 가장 잘 알려져 있다.
하위 도메인(subdomain)이란?
비즈니스 도메인의 목표를 달성하기 위해 기업은 여러 가지 하위 도메인을 운영해야 한다. 하위 도메인은 비즈니스 활동이 세분화된 영역이다. 예를 들어, 스타벅스는 커피로 가장 잘 알려져 있지만, 커피를 만드는 일 외에 좋은 위치의 부동산을 구매하거나 임대하고, 직원을 고용하고, 재정을 관리해야 한다. 이러한 하위 도메인 중 어느것도 자체적으로 수익을 낼 수는 없다.
하위 도메인 유형: 핵심, 일반, 지원의 세 가지 유형으로 구분
- 핵심 하위 도메인(core subdomain): 경쟁업체와 다르게 수행하고 있는 것
- 예: 구글 애즈(Google Ads)/비즈니스 도메인 - 순위 알고리즘/핵심 하위 도메인 (우수한 검색 결과를 제공하는 능력은 트래픽을 유도하는 요소이며, 결과적으로 광고 플랫폼의 중요한 구성요소가 됨)
- 복잡성: 구현하기 쉬운 핵심 하위 도메인은 일시적인 경쟁 우위만 제공한다. 따라서 핵심 하위 도메인은 자연스럽게 복잡해진다. 회사의 핵심 비즈니스는 높은 진입장벽이 있어야 한다. 경쟁사가 회사의 솔루션을 모방하거나 복제하기가 어려워야 한다.
- 경쟁 우위의 원천: 핵심 하위 도메인에 반드시 기술이 들어가야 하는 것은 아님을 알아야 한다. 회사의 경쟁 우위는 다양한 원천에서 나올 수 있다.
- 일반 하위 도메인(generic subdomain): 모든 회사가 같은 방식으로 수행하는 비즈니스 활동
- 예: 사용자를 인증하고 권한을 부여하는 작업
- 핵심 하위 도메인과 마찬가지로 복잡하고 구현하기 어렵다. 그러나 일반 하위 도메인은 회사에 경쟁력을 제공하지 않는다. 이미 실무에서 검증된 솔루션으로 널리 이용 가능하며, 모든 회사에서 사용하고 있어서 더 이상 혁신이나 최적화가 필요 없다.
- 지원 하위 도메인(supporting subdomain): 회사의 비즈니스를 지원하는 활동
- 예: 광고 매칭, 최적화를 핵심 하위 도메인으로 갖고 있는 온라인 광고 회사 - 배너와 랜딩 페이지 같은 자료를 물리적으로 저장하고 인덱싱하는 시스템/지원 하위 도메인
- 지원 하위 도메인의 비즈니스 로직은 대부분 대부분 데이터 입력 화면과 ETL(extract, transform, load; 추출, 변환, 로드) 작업과 유사하다. 소위 말하는 CRUD 인터페이스를 말한다.
하위 도메인 비교
- 핵심 하위 도메인과 지원 하위 도메인 구별: 하위 도메인을 부업으로 전환할 수 있는지 물어보자. 기꺼이 비용을 지불할 의사가 있는가? 그렇다면 핵심 하위 도메인이다.
- 일반 하위 도메인과 지원 하위 도메인 구별: 외부 솔루션을 연동하는 것보다 자체 솔루션을 구현하는 것이 더 간단하고 저렴한가? 그렇다면 지원 하위 도메인이다.
솔루션 전략
- 기업이 해당 비즈니스 도메인에서 일하려면 하위 도메인 모두가 필요하다.
- 핵심 하위 도메인은 사내에서 구현되어야 한다. 핵심 하위 도메인은 솔루션을 구매하거나 외부에서 도입할 수 없다. 그럴 경우 경쟁업체들이 똑같이 할 수 있기 때문에 경쟁 우위 개념을 약화시킬 것이다. 또한 핵심 하위 도메인 구현을 하청하는 것도 현명하지 않다. 조직의 가장 숙련된 인재는 핵심 하위 도메인에서 일하도록 업무가 할당되어야 한다. 핵심 하위 도메인의 요구사항은 자주 그리고 지속적으로 변경될 것으로 예상되므로 솔루션은 유지보수가 가능하고 쉽게 개선될 수 있어야 한다.
- 일반 하위 도메인을 사내에서 구현하는 데 시간과 노력을 투자하는 것보다 이미 만들어진 제품을 구입하거나 오픈소스 솔루션을 채택하는 것이 비용 면에서 더 효율적이다.
- 지원 하위 도메인 구현은 비즈니스 로직이 단순하고 변경의 빈도가 적기 때문에 원칙을 생략하고 적당히 진행하기 쉽다. 아주 짧은 주기로 신속한 애플리케이션 개발(RAD: rapid application development) 프레임워크를 사용해서 우발적 복잡성(accidental complexity) 없이 비즈니스 로직을 구현하기에 충분하다.
도메인 분석 예제
Gigmaster: 티켓 판매 및 유통 회사다. 모바일 앱은 사용자의 음악 라이브러리, 스트리밍 서비스 계정, 소셜 미디어 프로필을 분석하여 사용자가 관심을 가질 만한 주변의 공연 정보를 찾아낸다. 사용자의 개인정보는 암호화된다. 또한 추천 알고리즘은 익명 데이터만 사용한다. Gigmaster를 통해 티켓을 구매하지 않았더라도 사용자가 과거에 참석한 공연 정보를 기록할 수 있다.
- 비즈니스 도메인: 티켓 판매
- 하위 도메인
- 핵심 하위 도메인: 추천 엔진, 데이터 익명화, 모바일 앱
- 일반 하위 도메인: 암호화, 회계, 정산, 인증 및 권한 부여
- 지원 하위 도메인: 음악 스트리밍 서비스와 연동, 소셜 네트워크 연동, 참석 공연 모듈
2. 도메인 지식 찾아내기
커뮤니케이션
- 소프트웨어 프로젝트가 실패하는 이유에 대한 연구에서 효과적인 커뮤니케이션이 지식 공유와 프로젝트 성공에 필수라는 것을 밝혀냈다.
유비쿼터스 언어랑 무엇인가?
- 효과적으로 소통하기 위해 변환에 의존하지 말고 같은 언어를 사용하는 것이다. 비즈니스 도메인을 설명하기 위한 단일화된 언어 체계가 유비쿼터스 언어다.
비즈니스 도메인 모델
- 효과적인 모델링: 모든 모델에는 목적이 있고 그 목적을 달성하는 데 필요한 세부사항만 포함한다. 모델은 본질적으로 추상화의 결과다. 추상화 개념은 불필요한 상세 정보를 생략하여 복잡한 문제를 다룰 수 있게 하고 당면한 문제를 푸는 데 필요한 정보만 남게 한다. 반면, 비효과적인 추상화는 필요한 정보를 제거하거나 필요 없는 정보를 포함해 잡음을 유발한다.
3. 도메인 복잡성 관리
일관성 없는 모델: 예를 들어 같은 단어인 리드(lead)를 영업 부서와 마케팅 부서에서 서로 다른의미로 사용되는 경우
바운디드 컨텍스트(bounded context)란 무엇인가?
- 유비쿼터스 언어를 여러 개의 작은 언어로 나눈 다음 각 언어를 적용할 수 있는 명시적인 바운디드 컨텍스트에 할당하면 된다.
- 앞의 예에서 마케팅과 영업이라는 두 가지 바운디드 컨텍스트를 식별할 수 있다.
모델 경계
- 모델은 경계 없이 존재할 수 없다. 경계가 없다면 현실 세계의 복제본처럼 확장될 것이다. 따라서 모델의 경계(바운디드 컨텍스트)를 정의하는 것은 모델링 프로세스의 본질적인 부분이다.
바운디드 컨텍스트의 범위
- 비즈니스 도메인을 모델링하기 위해 모델을 분할하고, 각 세분화된 모델에 적용 가능한 컨텍스트(바운디드 컨텍스트)를 엄격하게 정의해야 한다.
- 모델을 더 작은 바운디드 컨텍스트로 분해할 수 있다.
- 모델의 크기에 정답은 없다. 다만 모델 자체로 유용해야 한다. 유비쿼터스 언어의 경계가 넓을수록 일관성을 유지하기가 더 어렵다.
- 바운디드 컨텍스트를 작게 만들기 위해 노력하는 것은 역효과를 낼 수 있다. 더 작게 만들수록 설계를 통합하는 오버헤드가 커진다.
- 주의할 점은 응집된 기능을 여러 바운디드 컨텍스트로 분할하는 것이다. 이러한 분할 방법은 각 컨텍스트가 독립적으로 발전하는 능력을 저해한다.
경계
- 바운디드 컨텍스트 간의 명확한 물리적 경계를 통해 각 바운디드 컨텍스트를 요구사항에 가장 적합한 기술 스택으로 구현할 수 있다.
- 바운디드 컨텍스트는 여러 하위 도메인을 포함할 수 있다. 이러한 경우 바운디드 컨텍스트는 물리적 경계고 하위 도메인은 논리적 경계다. 논리적 경계는 프로그래밍 언어의 종류에 따라 네임스페이스나 모듈, 패키지 같은 다른 이름을 갖는다.
결론
- 유비쿼터스 언어는 바운디드 컨텍스트의 범위 내에서 일관성이 있어야 한다. 그러나 서로 다른 바운디드 컨텍스트에서는 동일한 용어라도 다른 의미를 가질 수 있다.
- 하위 도메인이 발견되면 바운디드 컨텍스트도 설계한다. 도메인을 바운디드 컨텍스트로 나누는 것은 전략적 설계의 의사결정이다.
4. 바운디드 컨텍스트 연동
- 바운디드 컨텍스트 사이에는 항상 접점이 있는데 이것을 컨트랙트(contract)라고 부른다.
- 각 컨트랙트는 하나 이상의 당사자에 영향을 끼치므로 서로 조율해서 컨트랙트를 정의해야 한다.
협력형(cooperation) 패턴 그룹
소통이 잘 되는 팀에서 구현된 바운디드 컨텍스트와 관련이 있다.
- 파트너십(partnership) 패턴
- 바운디드 컨텍스트 간의 연동은 애드혹(ad-hoc) 방식으로 조정한다. 한 팀은 다른 팀에게 API 변경을 알리고 다른 팀은 충돌 없이 이를 받아들인다.
- 공유 커널 패턴
- 바운디드 컨텍스트가 모델의 경계임에도 불구하고, 여전히 하위 도메인의 동일 모델 혹은 그 일부가 여러 다른 바운디드 컨텍스트에서 구현되는 경우가 있다. 공유 커널(shared kernel)과 같은 공유 모델은 모든 바운디드 컨텍스트의 필요에 따라 설계된다는 점을 강조하고 싶다. 더구나 공유 모델은 이를 사용하는 모든 바운디드 컨텍스트에 걸쳐서 일관성을 유지해야 한다.
- 공유 모델의 변경은 다른 모든 바운디드 컨텍스트에 즉시 영향을 준다. 그러므로 변경의 연쇄 영향을 최소화하려면 양쪽의 겹치는 모델을 제한해서 바운디드 컨텍스트에서 공통으로 구현돼야 하는 모델의 일부분만 노출하도록 해야 한다.
- 공유 커널을 사용해야 하는 경우
- 공유 커널 패턴의 적용 여부를 결정하는 가장 중요한 중복 비용과 조율 비용의 비율이다. 이 패턴을 적용한 바운디드 컨텍스트 간에 강한 의존관계를 만들기 때문에 중복 비용이 조율 비용보다 클 경우에만 적용해야 한다.
- 공유 커널을 사용하는 데는 명분이 필요하다. 이것은 신중하게 고려해야 하는 실용적인 에외로 볼 수 있다.
- 통합 문제를 일찍 발견하는 방법은 공유 커널의 범위를 최소화해 연쇄적인 변경의 범위를 줄이고 매번 변경할 때마다 통합 테스트를 돌리는 것이다.
사용자-제공자(customer-supplier) 패턴 그룹
- 제공자는 사용자에게 서비스를 제공한다. 서비스 제공자는 ‘업스트림(upstream)’이고 고객 또는 사용자는 ‘다운스트림(downstream)’이다.
- 대부분의 경우 연동 컨트랙트를 주도하는 권력의 불균형이 존재한다.
- 힘의 차이를 보여주는 세 가지 패턴
- 순응주의자 패턴
- 힘의 균형이 서비스를 제공하는 업스트림 팀에 있는 경우
- 다운스트림 팀이 업스트림 팀의 모델을 받아들이는 바운디드 컨텍스트의 관계를 순응주의자(conformist) 패턴이라고 부른다.
- 충돌 방지 계층(ACL, anticorruption layer) 패턴
- 다운스트림 바운디드 컨텍스트가 순웅하지 않는 경우, 충돌 방지 계층을 통해 업스트림 바운디드 컨텍스트의 모델을 스스로의 필요에 맞게 가공할 수 있다.
- 오픈 호스트 서비스(OHS, open-host service) 패턴
- 힘이 사용자 측에 있을 경우
- 구현 모델의 변경으로부터 사용자를 보호하기 위해 업스트림 제공자는 퍼블릭 인터페이스와 구현 모델을 분리한다.
- 제공자의 퍼블릭 인터페이스는 자신의 유비쿼터스 언어를 따르는 대신, 연동 지향 언어(integration-oriented language)를 통해 사용자에게 더 편리한 프로토콜을 노출한다. 이런 퍼블릭 프로토콜을 공표된 언어(published language)라고 한다.
- 충돌 방지 계층 패턴의 반대다. 사용자 대신 제공자가 내부 모델 번역을 구현한다.
- 순응주의자 패턴
분리형 노선(separated ways)
- 협업 의지가 없거나 협업할 수 없는 경우
- 커뮤니케이션 이슈
- 일반 하위 도메인
- 모델의 차이: 모델이 너무 달라서 순응주의자 관계가 불가능하고 충돌 방지 계층을 구현하는 것도 기능 중복보다 비용이 더 클 수 있다. 이런 경우 팀이 각자의 길을 가는 것이 더 비용 효과적이다.
- 핵심 하위 도메인을 연동할 경우에는 협업 없는 분리형 노선은 피해야 한다. 하위 도메인 중복 구현은 회사의 전략을 효과적이고 효율적으로 구현하는 것을 어렵게 한다.
5. 간단한 비즈니스 로직 구현
단순한 비즈니스 로직에 적합한 두 가지 패턴인 트랜잭션 스크립트와 액티브 레코드부터 시작해보자.
트랜잭션 스크립트
- 시스템의 퍼블릭 인터페이스 = 사용자가 실행할 수 있는 비즈니스 트랜잭션 모음
- 트랜잭션 스크립트 패턴은 프로시저를 기반으로 시스템의 비즈니스 로직을 구성하며, 각 프로시저는 퍼블릭 인터페이스를 통해 시스템 사용자가 실행하는 작업을 구현한다.
- 프로시저가 구현해야 하는 유일한 요구사항은 트랜잭션 동작이다. 각 작업은 성공하거나 실패할 수 있지만, 유효하지 않은 상태를 만들면 안 된다.
- 트랜잭션 스크립트를 올바르게 구현하지 못해 발생하는 데이터 손상의 실제 사례 세 가지
- 트랜잭션 동작 구현 실패
- 전체를 아우르는 트랜잭션 없이 여러 업데이트를 하는 경우
- 예를 들어 Users 테이블의 레코드를 업데이트하고 VisitsLog 테이블에 레코드를 삽입하는 경우, 두 데이터 변경을 모두 포함하는 트랜잭션을 만들어 해결할 수 있다.
public class LogVisit { public void Execute(Guid userId, DateTime visitedOn) { try { _db.StartTransaction(); _db.Execute("UPDATE Users SET last_visit=@visitedOn WHERE user_id = @userId"); _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date) VALUES (@userId, @visitedOn)"); _db.Commit() } catch { _db.Rollback(); } } }
- 분산 트랜잭션
- 최신 분산 시스템에서는 데이터베이스의 데이터를 변경한 다음 메시지 버스에 메시지를 발행하여 시스템의 다른 컴포넌트에 변경사항을 알리는 것이 일반적이다.
- 앞의 예에서 User 테이블의 레코드를 업데이트하고 VisitLog 테이블에 레코드를 삽입하기 위해 메시지를 발행하는 경우, CQRS 아키텍처 패턴을 사용하거나 아웃박스 패턴(outbox pattern)을 사용해 해결할 수 있다.
public class LogVisit { public void Execute(Guid userId, DateTime visitedOn) { _db.Execute("UPDATE Users SET last_visit=@visitedOn WHERE user_id = @userId"); _messageBus.Publish("VISIT_TOPIC", new { UserId = userId, VisitDate = visitedOn }); } }
- 암시적 분산 트랜잭션
- 이전 예제에서 마지막 방문 날짜를 추적하는 각 사용자에 대한 방문 카운터를 유지한다. 메서드를 호출하면 해당 카운터의 값이 1씩 증가한다.
public class LogVisit { public void Execute(Guid userIdvisitedOn) { _db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id = @userId"); } }
- 잠재적으로 일관성 없는 상태로 이어질 수 있는 분산 트랜잭션이다.
- LogVisit이 Rest 서비스의 일부이고 LogVisit 호출자가 있을 때 오류가 발생하면, LogVisit 호출을 다시 시도하고 카운터 값이 잘못 증가하는 것이다.
- 이 문제를 간단하게 수정하는 방법은 없다. 모두 비즈니스 도메인과 해당 요구사항에 따라 달라진다. 이 예제에서 트랜잭션 동작을 보장하는 한 가지 방법은 작업을 멱등성(idempotent)으로 만드는 것이다. 즉, 같은 요청을 여러 번 반복하더라도 그 결과는 매번 동일하게 만드는 것이다.
public class LogVisit { public void Execute(Guid userId, long visits) { _db.Execute("UPDATE Users SET visits=@visits WHERE user_id = @userId"); } }
- 또 다른 방법은 낙관적 동시성 제어(optimistic concurrency control)를 사용하는 것이다.
- LogVisit 작업을 호출하기 전에 호출자는 카운터의 현재 값을 매개변수로 전달한다. 이 매개변수 값과 동일한 경우에만 카운터 값을 업데이트한다.
public class LogVisit { public void Execute(Guid userId, long expectedVisits) { _db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id = @userId AND visits = @visits"); } }
- 트랜잭션 동작 구현 실패
트랜잭션 스크립트를 사용하는 경우
트랜잭션 스크립트 패턴은 매우 간단한 문제 도메인에 효과적이다. 비즈니스 로직이 단순한 지원 하위 도메인에 적합하다. 또한 일반 하위 도메인과 같은 외부 시스템과 연동하기 위한 어댑터로 사용하거나 충돌 방지 계층의 일부로 사용할 수도 있다.
트랜잭션 스크립트 패턴의 주요 장점은 단순함이다. 최소한의 추상화를 도입하여 런타임 성능을 최적화하고, 비즈니스 로직을 이해하기 위한 최소화한다. 하지만, 비즈니스 로직이 복잡할수록 트랜잭션 간에 비즈니스 로직이 중복되기 쉽고 결과적으로 중복된 코드가 동기화되지 않을 때 일관성 없는 동작이 발생한다.
이러한 단순함으로 인해 트랜잭션 스크립트는 항상 좋다고 할 수 없으며, 때로는 안티 패턴으로 취급되기도 한다.
액티브 레코드
- 비즈니스 로직이 단순한 경우 사용한다.
- 좀 더 복잡한 자료구조에서도 비즈니스 로직이 작동할 수 있다.
- 액티브 레코드라고 하는 적용 객체를 사용하여 복잡한 자료구조를 표현한다. 자료구조 외에도 이러한 객체는 레코드 생성, 읽기, 업데이트, 삭제를 위한 데이터 접근 방법도 구현한다.
- 액티브 레코드 객체는 객체 관계 매핑(ORM: object-relational mapping) 또는 다른 데이터 접근 프레임워크와도 관련이 있다. 각 자료구조가 ‘액티브(active)’하다는 점에서 패턴의 이름이 만들어졌다.
public class CreateUser
{
public void Execute(userDetails)
{
_db.StartTransaction();
var user = new User();
user.Name = userDetails.Name;
user.Email = userDetails.Email;
user.Save();
_db.Commit();
} catch
{
_db.Rollback();
throw;
}
}
- 트랜잭션 스크립트와 차이점은 액티브 레코드의 경우 데이터베이스에 직접 접근하는 대신 트랜잭션 스크립트가 액티브 레코드 객체를 조작한다는 것이다.
- 액티브 레코드 객체의 고유한 기능은 자료구조와 동작(비즈니스 로직)의 분리다. 일반적으로 액티브 레코드의 필드에는 외부 프로시저가 상태를 수정할 수 있게 하는 퍼블릭 게터(getter)와 세터(setter)가 있다.
액티브 레코드를 사용하는 경우
- 본질적으로 데이터베이스에 대한 접근을 최적화하는 트랜잭션 스크립트이기 때문에 이 패턴은 기껏해야 사용자 입력의 유효성을 검사하는 CRUD 작업과 같은 비교적 간단한 비즈니스 로직만 지원할 수 있다.
- 따라서 트랜잭션 스크립트 패턴과 마찬가지로 지원 하위 도메인, 일반 하위 도메인과 외부 솔루션의 연동, 모델 변환 작업에 적합하다. 두 패턴의 차이점은 복잡한 자료구조를 데이터베이스 스키마에 매핑하는 복잡성을 해소한다는 것이다.
- 비즈니스 로직이 단순할 때 액티브 레코드를 사용하는 데는 아무런 문제가 없다. 또한 단순한 비즈니스 로직을 구현할 때 보다 정교한 패턴을 사용하는 것도 우발적 복잡성을 일으켜 해를 끼칠 수 있다.
6. 복잡한 비즈니스 로직 다루기
- 에반스는 비즈니스 도메인의 하위 모델과 코드를 긴밀하게 연결 짓는 데 쓰이는 애그리게이트(aggregate), 밸류 오브젝트(value object), 리포지토리(repository) 등과 같은 패턴을 제시했다.
- 이 패턴이 ‘도메인 모델’이고 애그리게이트와 밸류 오브젝트는 그 구성요소다.
도메인 모델
도메인 모델 패턴은 복잡한 비즈니스 로직을 다루기 위한 것이다. CRUD 인터페이스 대신 복잡한 상태 전환, 항상 보호해야 하는 규칙인 비즈니스 규칙과 불변성을 다룬다.
- 구현: 도메인 모델은 행동(behavior)과 데이터(data) 모두를 포함하는 도메인의 객체 모델이다.
- 복잡성: 모델에는 데이터베이스 또는 외부 시스템 구성요소의 호출 구현 같은 인프라 또는 기술적인 관심사를 피해야 한다. 이 같은 제약을 따르면 모델의 객체는 플레인 올드 오브젝트(plain old object)가 된다.
- 구성요소
-
밸류 오브젝트: 복합적인(composition) 값에 의해 식별되는 객체
class Color { int _red; int _green; int _blue; }
- 언어의 표준 라이브러리에 포함된 문자열(string), 정수(integer), 딕셔너리(dictionary) 같은 원시 데이터 타입에 전적으로 의존해서 비즈니스 도메인의 개념을 표현하는 것은 원시 집착 코드 징후(primitive obsession code smell)로 알려져 있다.
- 클래스가 모든 입력 필드를 검사해야 한다.
- 유효성 검사 로직이 중복되기 쉽다.
- 값이 사용되기 전에 유효성 검사 로직을 호출하게 하기 어렵다.
- 다른 엔지니어가 코드베이스를 개선하는 것과 같은 미래를 대비한 유지보수가 더 어렵다.
public class Person { private int _id; private string _firstName; private string _lastName; private string _landlinePhone; // 유선 전화번호 private string _mobilePhone; private string _email; private int _heightMetric; private string _countryCode; // 두 자리 대문자로 된 국가 코드 public Person(int id, string firstName, string lastName, string landlinePhone, string mobilePhone, string email, int heightMetric, string countryCode) { _id = id; _firstName = firstName; _lastName = lastName; _landlinePhone = landlinePhone; _mobilePhone = mobilePhone; _email = email; _heightMetric = heightMetric; _countryCode = countryCode; } } var person = new Person(30217, "John", "Doe", "0212345678", "01012345678", "john.doe@email.com", 175, "KR");
- 밸류 오브젝트를 사용하는 설계
- 명료성 향상: 짧은 변수 이름을 사용하더라도 의도를 명확하게 전달한다.
- 유효성 검사 로직이 밸류 오브젝트 자체에 들어 있기 때문에 값을 할당하기 전에 유효성 검사를 할 필요가 없다.
- 게다가 밸류 오브잭트는 값을 조작하는 비즈니스 로직을 한곳에 모을 때 더욱 진가를 발휘한다. 이렇게 응집된 로직은 한곳에서 구현되고 쉽게 테스트할 수 있따.
- 밸류 오브젝트를 사용하면 코드에서 유비쿼터스 언어를 사용하게 하므로 코드에서 비즈니스 도메인의 개념을 표현하게 된다.
public class Person { private PersonId _id; private Name _name; private PhoneNumber _landline; private PhoneNumber _mobile; private EmailAddress _email; private Height _height; private CountryCode _country; public Person(PersonId id, Name name, PhoneNumber landline, PhoneNumber mobile, EmailAddress email, Height height, CountryCode country) { _id = id; _name = name; _landline = landline; _mobile = mobile; _email = email; _height = height; _country = country; } } var id = new PersonId(30217); var name = new Name("John", "Doe"); var landline = PhoneNumber.Parse("02-1234-5678"); var mobile = PhoneNumber.Parse("010-1234-5678"); var email = EmailAddress.Parse("john.doe@email.com"); var height = Height.FromMetric(175); var country = CountryCode.Parse("KR"); var person = new Person(id, name, landline, mobile, email, height, country);
- 밸류 오브젝트 구현
- 불변의 객체로 구현되므로 밸류 오브젝트에 있는 필드가 하나라도 바뀌면 다른 값이 생성된다.
- 동일성은 id 필드나 참조 대신 값을 기반으로 하므로 동일성 검사 함수를 오버라이드해서 적절히 구현하는 것이 중요하다. (C#에서 record 타입은 값 기반의 동일성 검사를 구현하므로 동일성 연산자를 오버라이드 할 필요가 없다.)
- 밸류 오브젝트를 사용하는 경우: 가능한 모든 경우에 사용하는게 좋다. 코드의 표현력을 높여주고 분산되기 쉬운 비즈니스 로직을 한데 묶어줄 뿐만 아니라 코드를 더욱 안전하게 해준다. 밸류 오브젝트는 불변이기 때문에 내포된 동작은 부작용과 동시성 문제가 없다.
- 언어의 표준 라이브러리에 포함된 문자열(string), 정수(integer), 딕셔너리(dictionary) 같은 원시 데이터 타입에 전적으로 의존해서 비즈니스 도메인의 개념을 표현하는 것은 원시 집착 코드 징후(primitive obsession code smell)로 알려져 있다.
- 애그리게이트: 트랜잭션 경계를 공유하는 엔티티 계층이다. 애그리게이트의 경계에 속하는 모든 데이터 비즈니스 로직의 구현을 통해 강력한 일관성을 유지해야 한다.
- 엔티티: 밸류 오브젝트와 정반대다.
- 엔티티는 다른 엔티티 인스턴스와 구별하기 위해 명시적인 식별 필드가 필요하다. 식별 필드의 핵심 요구사항은 각 엔티티의 인스턴스마다 고유해야 한다는 것이다.
- 예: 식별 필드(id) - 값(guid, 주민등록번호, …)
- 엔티티의 식별 필드의 값은 엔티티의 생애주기 내내 불변이어야 한다.
- 밸류 오브젝트와는 반대로, 엔티티는 불변이 아니고 변할 것으로 예상된다.
- 밸류 오브젝트는 엔티티의 속성을 설명한다.
- 엔티티는 다른 엔티티 인스턴스와 구별하기 위해 명시적인 식별 필드가 필요하다. 식별 필드의 핵심 요구사항은 각 엔티티의 인스턴스마다 고유해야 한다는 것이다.
- 애그리게이트는 엔티티다.
- 애그리게이트는 단순한 엔티티가 아닌 그 이상이다. 이 패턴의 목적은 데이터의 일관성을 보호하는 데 있다.
- 일관성 강화
- 애그리게이트는 일관성을 강화하는 경계다. 애그리게이트의 로직은 모든 들어오는 변경 요청을 검사해서 그 변경이 애그리게이트의 비즈니스 규칙에 위배되지 않게 해야 한다.
- 애그리게이트의 퍼블릭 인터페이스로 노출된 상태 변경 메서드는 커맨드라고 부른다.
- 커맨드는 두 가지 방식으로 구현할 수 있다.
- 애그리게이트 객체에 평범한 퍼블릭 메서드로 구현
- 커맨드 실행에 필요한 모든 입력값을 포함하는 파라미터 객체로 표현하는 것 (다형적 구현 가능)
- 애그리게이트를 저장하는 데이터베이스에서 동시성 관리를 지원해야 한다. 가장 간단한 형태는 매번 갱신할 때마다 증가하는 버전 필드를 애그리게이트에서 관리하는 것이다.
- 트랜잭션 경계: 트랜잭션별로 하나의 애그리게이트 인스턴스만 갖게 제한하면 애그리게이트의 경계가 비즈니스 도메인의 불변성과 규칙을 따르도록 신중히 설계하게 된다.
- 엔티티 계층: 엔티티는 독립적 패턴이 아닌 애그리게이트의 일부로서만 사용된다.
- 다른 애그리게이트 참조하기
- 애그리게이트 내의 모든 객체는 같은 트랜잭션 경계를 공유하기 때문에 애그리게이트가 너무 커지면 성능과 확장 문제가 생길 수 있다.
- 데이터의 일관성은 애그리게이트의 경계를 설계하는 데 편리한 가이드 원칙이다. 애그리게이트의 비즈니스 로직에 따라 강력한 일관성이 필요한 정보만 애그리게이트에 포함돼야 한다.
- 애그리게이트를 가능한 한 작게 유지하고 애그리게이트의 비즈니스 로직에 따라 강력하게 일관적으로 상태를 유지할 필요가 있는 객체만 포함한다.
- 애그리게이트 루트: 애그리게이트 상태는 커맨드 중 하나를 실행해서만 수정할 수 있다. 애그리게이트가 엔티티의 계층 구조를 대표하기 때문에 그 중 하나만 애그리게이트의 퍼블릭 인터페이스, 즉 애그리게이트 루트로 지정돼야 한다.
- 도메인 이벤트: 비즈니스 도메인에서 일어나는 중요한 이벤트를 설명하는 메시지
- 도메인 이벤트는 이미 발생된 것이기 때문에 과거형으로 명명한다.
- 도메인 이벤트의 목적은 비즈니스 도메인에서 일어난 일을 설명하고 이벤트와 관련된 모든 필요한 데이터를 제공하는 것이다.
- 도메인 이벤트는 애그리게이트의 퍼블릭 인터페이스의 일부다. 애그리게이트는 자신의 도메인 이벤트를 발행한다.
- 다른 프로세스, 애그리게이트, 심지어 외부 시스템도 이 도메인 이벤트를 구독할 수 있고 도메인 이벤트에 반응하는 자신만의 로직을 실행할 수도 있다.
- 엔티티: 밸류 오브젝트와 정반대다.
- 도메인 서비스: 애그리게이트에도 밸류 오브젝트에도 속하지 않거나 복수의 애그리게이트에 관련된 비즈니스 로직을 다르게 될 경우, 도메인 서비스 로직을 구현할 것을 제안한다.
- 비즈니스 로직을 구현한 상태가 없는 객체(stateless object)다.
- 도메인 서비스는 여러 애그리게이트의 작업을 쉽게 조율할 수 있다. 그러나 한 개의 데이터베이스 트랜잭션에서 한 개의 애그리게이트 인스턴스만 수정할 수 있다고 했던 애그리게이트 패턴의 한계를 명심해야 한다. 도메인 서비스는 여러 애그리게이트의 데이터를 읽는 것이 필요한 계산 로직을 구현하는 것을 도와준다.
-
- 복잡성 관리
- 시스템의 자유도는 시스템의 상태를 설명하는 데 필요한 데이터 요소의 개수로 측정된다.
- 제어와 행동 예측의 관점에서 더 많은 자유도를 가진 클래스가 더 어렵다.
- 불변성이 복잡성을 낮춘다. 이것이 애그리게이트와 밸류 오브젝트 패턴이 하는 것이다. 복잡한 것을 불변성으로 감싸서 복잡성을 낮추는 것이다.
7. 시간 차원의 모델링
이벤트 소싱
- 이벤트 소싱 패턴은 데이터 모델에 시간 차원을 도입한다. 애그리게이트의 현재 상태를 반영하는 스키마 대신 이벤트 소싱 기반 시스템은 애그리게이트의 수명주기의 모든 변경사항을 문서화하는 이벤트를 유지한다.
-
이벤트 소싱 시스템에서 개인의 데이터가 표현되는 방식
{ "lead-id": 12, "event-id": 0, "event-type": "lead-initialized", "first-name": "Casey", "last-name": "David", "phone-number": "555-2951", "timestamp": "2020-05-20T09:52:55.95Z" }, { "lead-id": 12, "event-id": 1, "event-type": "contacted", "timestamp": "2020-05-20T12:32:08.24Z" }, { "lead-id": 12, "event-id": 2, "event-type": "followup-set", "followup-on": "2020-05-27T12:00:00.00Z", "timestamp": "2020-05-20T12:32:08.24Z" }, // ...
- 원천 데이터
- 이벤트 소싱 패턴이 작동하려면 객체 상태에 대한 모든 변경사항이 이벤트로 표현되고 저장되어야 한다. 이러한 이벤트 시스템의 원천 데이터가 된다.
- 이벤트를 저장하는 데 사용되는 데이터베이스를 지칭하는 이름이 이벤트 스토어(event store)다.
- 이벤트 스토어: 추가만 가능한 저장소이므로 이벤트를 수정하거나 삭제할 수 없다.
- 이벤트 소싱 패턴은 새로운 것이 아니다. 금융 산업에서는 이벤트를 사용하여 원장의 변경사항을 나타탠다. 원장은 트랜잭션을 문서화하는 추가 전용 로그다. 계정 잔액과 같은 현재 상태는 원장의 기록을 ‘프로젝션’해서 언제든지 추론할 수 있다.
이벤트 소싱 도메인 모델
- 원래 도메인 모델은 애그리게이트의 상태 표현 방식을 유지 관리하고 선택 도메인 이벤트를 내보낸다. 이벤트 소싱 도메인 모델은 애그리게이트의 수명주기를 모델링하기 위해 독점적으로 도메인 이벤트를 사용한다. 애그리게이트 상태에 대한 모든 변경사항은 도메인 이벤트로 표현돼야 한다.
- 작업 단계
- 애그리게이트의 도메인 이벤트를 로드한다.
- 이벤트를 비즈니스 의사결정을 내리는 데 사용할 수 있는 상태로 프로젝션해서 상태 표현을 재구성한다.
- 프로젝션: 이벤트 소싱 패턴에서 쓰기 모델을 통해 이벤트 소싱 시스템에 이력 형태로 저장된 데이터를 다양한 읽기 모델을 적용해 원하는 시점의 데이터를 추출하는 기법
- 애그리게이트의 명령을 실행하여 비즈니스 로직을 실행하고 결과적으로 새로운 도메인 이벤트를 생성한다.
- 새 도메인 이벤트를 이벤트 스토어에 커밋한다.
- 장점
- 시간 여행: 모든 과거 상태를 복원하는 데 사용할 수 있다.
- 시스템 동작을 분석하고, 시스템의 의사결정을 검사하고, 비즈니스 로직을 최적화할 때 종종 필요하다.
- 소급 디버깅(retroactive debugging): 버그가 관찰됐을 때의 상태로 되돌릴 수 있다.
- 심오한 통찰력: 기존 이벤트의 데이터를 활용하여 추가 통찰력을 제공할 새로운 프로젝션 방법을 언제든지 추가할 수 있다.
- 감사 로그(audit log): 법률에 따라 일부 비즈니스 도메인은 감사 로그를 반드시 구현해야 하며 이벤트 소싱은 이를 즉시 제공한다.
- 고급 낙관적 동시성 제어: 읽기 데이터가 기록되는 동안 다른 프로세스에 의해 덮어 쓰여지는 경우 예외를 발생시킨다. 이벤트 스토어에 동시에 추가된 정확한 이벤트를 추출하고 새로운 이벤트가 시도된 작업과 충돌하는지, 또는 추가 이벤트가 관련이 없고 계속 진행하는 것이 안전한지에 대해 비즈니스 도메인 주도 의사결정을 내릴 수 있다.
- 시간 여행: 모든 과거 상태를 복원하는 데 사용할 수 있다.
- 단점
- 학습 곡선
- 모델의 진화: 이벤트 소싱 모델을 발전시키는 것은 어려울 수 있다. 이벤트 소싱의 정의를 엄밀하게 따지면 이벤트는 변경할 수 없다.
- 아키텍처 복잡성: 이벤트 소싱을 구현하면 수많은 아키텍처의 ‘유동적인 부분’이 도입되어 전체 설계가 더 복잡해진다.
자주 묻는 질문
- 성능
- 이벤트를 상태 표현 방식으로 프로젝션하려면 실제로 컴퓨팅 성능이 필요하며 애그리게이트 목록에 더 많은 이벤트가 추가됨에 따라 그 필요성은 더 커진다.
- 대부분의 시스템에서 애그리게이트당 10,000개 이상의 이벤트가 있을 경우 성능 저하가 눈에 띄게 나타난다. 그러나 대다수의 시스템에서 애그리게이트의 평균 수명은 100개 이벤트를 초과하지 않는다.
- 상태를 프로젝션하는 것이 성능에 문제가 되는 경우, 스냅숏 패턴 같은 다른 패턴을 적용할 수 있다.
- 프로세스는 이벤트 스토어에서 새 이벤트를 지속적으로 순회하고 해당 프로젝션을 생성하고 캐시에 저장한다.
- 시스템의 애그리게이트가 10,000개 이상의 이벤트를 저장하지 않는 경우 스냅숏 패턴을 구현하는 것은 시스템을 복잡하게 만들 뿐이다. 그러므로 계속해서 스냅숏 패턴을 구현하기 전에 애그리게이트의 경계를 다시 확인하는 것이 필요하다.
- 이 모델은 엄청난 양의 데이터를 생성한다. 확장할 수 있을까?
- 이벤트 스토어는 애그리게이트 ID로 분할할 수 있다. 애그리게이트의 인스턴스 속에 속하는 모든 이벤트는 단일 샤드(shard)에 있어야 한다.
- 텍스트 파일에 로그를 작성하여 감사 로그를 사용할 수 없는 이유는 무엇일까? 실시간 데이터 처리 데이터베이스와 로그 파일 모두에 데이터를 쓰는 것은 오류가 발생하기 쉬운 작업이다. 데이터베이스와 파일이라는 두 가지 저장 장치에 대한 트랜잭션이다. 예를 들어, 데이터베이스 트랜잭션이 실패하면 아무도 이전 로그 메시지를 삭제하지 않는다. 따라서 이러한 로그는 결국 일관성이 없어진다.
- 상태 기반 모델을 계속 사용할 수 없지만 동일한 데이터베이스 트랜잭션에서 로그를 로그 테이블에 추가할 수 없는 이유는 무엇일까? 인프라 관점에서 이 접근 방식은 상태와 로그 레코드 간의 일관된 동기화를 제공한다. 그러나 여전히 오류가 발생하기 쉽다. 미래에 작업할 엔지니어가 코드베이스에 적절한 로그 레코드를 추가하는 것을 잊어버리면 어떻게 될까? 또한 모든 필수 정보가 올바른 형식으로 작성되도록 강제할 방법이 없다.
8. 아키텍처 패턴
계층형 아키텍처(layered architecture)
- 가장 일반적인 아키텍처 패턴 중 하나다. 코드베이스를 수평 계층으로 조직하고, 각 계층은 사용자와 상호작용, 비즈니스 로직의 구현, 그리고 데이터의 저장과 같은 기술적 관심사 중 하나를 다룬다.
- 고전적인 형태의 계층형 아키텍처는 세 가지 계층으로 구성된다.
- 프레젠테이션 계층(PL: presentation layer): 시스템이 외부 환경으로부터 요청을 받고 결과를 소통하는 수단이다. 엄밀히 말하면 프로그램의 퍼블릭 인터페이스다. (웹 UI, CLI, REST API)
- 비즈니스 로직 계층(BLL: business logic layer): 소프트웨어의 중심이다. 액티브 레코드 또는 도메인 모델과 같은 비즈니스 로직 패턴을 이 계층에서 구현한다. (엔티티, 규칙, 프로세스)
- 데이터 접근 계층(DAL: data access layer): 데이터 접근 계층은 영속성 매커니즘에 접근할 수 있게 해준다. (데이터베이스, 메시지 버스, 오브젝트 스토리지, 외부 정보 제공자와 연동)
- 계층 간 커뮤니케이션
- 톱다운(top-down) 커뮤니케이션 모델에 따라 연동한다. 각 계층은 바로 아래 계층에만 의존한다.
- 변종: 계층형 아키텍처에 서비스 계층을 추가하는 것
- 서비스 계층: 프레젠테이션 계층과 비즈니스 로직 계층 사이의 중간 역할
- 서비스 계층을 명시적으로 갖추면 생기는 장점
- 동일한 서비스 계층을 여러 퍼블릭 인터페이스에서 재사용할 수 있다.
- 모든 관련 메서드를 한곳에 모으면 모듈화가 개선된다.
- 프리젠테이션 계층과 비즈니스 로직 계층의 결합도를 낮춘다.
- 비즈니스 기능을 테스트하기 쉬워진다.
- 서비스 계층을 명시적으로 갖추면 생기는 장점
- 서비스 계층: 프레젠테이션 계층과 비즈니스 로직 계층 사이의 중간 역할
- 용어
- 프레젠테이션 계층 = 사용자 인터페이스 계층
- 서비스 계층 = 애플리케이션 계층
- 비즈니스 로직 계층 = 도메인 계층 = 모델 계층
- 데이터 접근 계층 = 인프라스트럭처 계층
- 사용하는 경우
- 비즈니스 로직과 데이터 접근 계층 간에 의존성이 있다. 따라서 비즈니스 로직이 트랜잭셔 스크립트 또는 액티브 레코드 패턴을 사용하여 구현된 시스템에 계층형 아키텍처 패턴이 적합하다.
- 반면, 도메인 모델을 구현하는 데 계층형 아키텍처 패턴을 적용하는 것은 어렵다. 도메인 모델에서는 비즈니스 엔티티(애그리게이트와 밸류 오브젝트)가 하부의 인프라스트럭처에 대해 의존성이 없어야 하고 그것을 몰라야 하기 때문이다. 계층형 아키텍처를 적용해서 구현할 수 있지만 도메인 모델의 구현에는 다음에 논의할 패턴이 더 적합하다.
- 계층과 티어
- 계층형 아키텍처와 N-티어(N-Tier) 아키텍처는 혼동될 때가 많다. 두 패턴은 비슷하지만 계층과 티어는 개념적으로 다르다. 계층이 논리적 경계인 반면, 티어는 물리적 경계다.
포트와 어댑터(port & adapter)
- 용어
- 인프라스트럭처 계층: 본질적으로 프레젠테이션 계층과 데이터 접근 계층 모두 데이터베이스, 외부 서비스, 사용자 인터페이스 프레임워크 등의 외부 구성요소와 연동하는 것을 표현한다. 이들은 시스템의 비즈니스 로직을 반영하지 못하므로 이 같은 모든 인프라 관심사를 통합했다.
- 의존성 역전 원칙(DIP: dependency inversion principle)
- 전통적인 계층형 아키텍처에서 비즈니스 로직 계층은 인프라스트럭처 계층에 의존한다. DIP를 준수하기 위해 관계를 반대로 해야 한다.
- 포트와 어댑터 아키텍처의 전통적인 계층
- 비즈니스 로직 계층: 엔티티, 규칙, 프로세스
- 애플리케이션 계층: 액션
- 인프라스트럭처 계층: 데이터베이스, UI 프레임워크, 외부 제공자, 메시지 버스
- 인프라 구성요소의 연동
- 포트와 어댑터 아키텍처의 핵심 목적은 인프라스트럭처 구성요소로부터 시스템의 비즈니스 로직을 분리하는 것이다.
- 인프라스트럭처 구성요소를 직접 참조하고 호출하는 대신, 비즈니스 로직 계층은 인프라스트럭처 계층이 구현해야 할 ‘포트’를 정의한다. 인프라스트럭처 계층은 ‘어댑터’를 구현한다. 즉, 다양한 기술을 사용하기 위해 정의된 포트의 인터페이스를 구체적으로 구현한다.
- 추상 포트(abstract port)는 인프라스트럭처 계층에서 의존성 주입 또는 부트스트래핑을 통해 구체적인 어댑터로 나타난다.
- 변형: 포트와 어댑터 아키텍처는 헥사고날(hexagonal) 아키텍처, 어니언(onion) 아키텍처 그리고 클린(clean) 아키텍처로 알려졌다.
- 포트와 어댑터를 사용하는 경우: 모든 기술적 관심사로부터 비즈니스 로직을 분리하는 것이 포트와 어댑터 아키텍처의 목적이므로 이 아키텍처는 도메인 모델 패턴을 사용하여 구현한 비즈니스 로직에 매우 적합하다.
CQRS(command-query responsibility segregation)
CQRS 패턴은 포트와 어댑터와 동일한 비즈니스 로직과 인프라스트럭처 관심사에 기반한다.
- 폴리글랏 모델링
- 대부분의 경우 단일 비즈니스 도메인 모델로 시스템의 모든 요구사항을 해결하기는 어려울 수 있다.
- 완변한 데이터베이스는 없다. 모든 데이터베이스는 고유한 결점이 있다. 즉, 확장성이나 일관성 또는 지원하는 질의 모델 간에 균형이 필요하다.
- 완전한 데이터베이스의 대안으로 폴리글랏 영속성 모델(polyglot persistence model)이 있다. 다양한 데이터 관련 요구사항을 구현하기 위해 여러 데이터베이스를 사용하는 것이다.
- 예를 들어, 단일 시스템에서 실시간 데이터 처리 데이터베이스로 도큐먼트 저장소를 사용하거나 분석/보고용으로 칼럼 저장소, 그리고 검색 기능을 위해 검색 엔진을 사용할 수 있다.
- CQRS 패턴은 이벤트 소싱과 밀접하게 관련이 있다. 원래 CQRS는 이벤트 소싱 모델의 질의 한계를 극복하려고 정의됐다.
- 구현: 커맨드 실행 모델과 읽기 모델 두 유형이 있다.
- 커맨드 실행 모델(command execution model): 시스템의 상태를 수정하는 오퍼레이션(시스템 커맨드)를 전담으로 수행하는 단일 모델
- 읽기 모델(프로젝션): 캐시에서 언제든 다시 추출할 수 있는 프로젝션이다. 잘 구현된 CQRS에서는 모든 프로젝션의 모든 데이터를 삭제하고 처음부터 다시 재생성할 수 있다.
- 읽기 모델의 프로젝션
- 관계형 데이터베이스의 머터리얼라이즈 뷰(materialized view, 빈번한 질의의 결과를 물리 테이블에 저장해 성능을 높이는 매커니즘)의 개념과 유사하다.
- 원천 테이블이 갱신되면 변경사항은 미리 작성된 뷰에 반영되어야 한다.
- 프로젝션을 생성하는 두 가지 방식
- 동기식 프로젝션(synchronous projection): 격차 해소 구독 모델(catch-up subscription model)을 통해 OLTP 데이터의 변경사항을 가져온다.
- OLTP(Online Transcation Processing): 온라인 뱅킹, 쇼핑, 주문 입력 또는 텍스트 메시지 전송 등 동시에 발생하는 다수의 트랜잭션을 실행하는 데이터 처리 유형이다.
- 프로젝션 엔진이 OLTP 데이터베이스로부터 마지막에 처리했던 체크포인트 이후에 추가되거나 갱신된 레코드를 조회한다.
- 프로젝션 엔진이 조회된 데이터를 이용해서 시스템의 읽기 모델을 재생성 또는 갱신한다.
- 프로젝션 엔진은 마지막으로 처리 레코드의 체크포인트를 저장한다. 이 값은 다음 처리 때 추가되거나 갱신된 레코드를 조회하는 데 사용한다.
- 비동기식 프로젝션
- 커맨드 실행 모델은 모든 커밋된 변경사항을 메시지 버스에 발행한다. 프로젝션 엔진은 발행된 메시지를 구독하고 일기 모델을 갱신하는 데 사용한다.
- 도전과제: 비동기식 프로젝션 방식의 확실한 확장성과 성능의 장점에도 불구하고, 분산 컴퓨팅에서 문제가 발생하기 더 쉽다. 메시지의 순서가 잘못되거나 중복 처리되면 읽기 모델에 일관성 없는 데이터가 프로젝션된다. 또한 이 방식은 새로운 프로젝션을 추가하거나 이미 존재하는 것을 재생성하는 것이 어렵다. 그러므로 가능하면 동기식 프로젝션 방식을 구현하고, 그 위에 선택적으로 비동기식 프로젝션 방식을 추가하는 것을 권장한다.
- 동기식 프로젝션(synchronous projection): 격차 해소 구독 모델(catch-up subscription model)을 통해 OLTP 데이터의 변경사항을 가져온다.
- CQRS를 사용해야 하는 경우: 다양한 종류의 데이터베이스에 저장된 동일한 데이터와 작동할 필요가 있는 애플리케이션에 유용하다. 또한 이벤트 소싱 도메인 모델에도 적합하다.
9. 커뮤니케이션 패턴
바운디드 컨텍스트 간 커뮤니케이션을 용이하게 하고, 애그리케이트 설계 원칙에 의해 부과된 제한 사항을 해결하고, 여러 시스템 컴포넌트에 걸쳐 비즈니스 프로세스를 조율한다.
모델 변환
- 프로토콜은 임시방편식으로 조정될 수 있고 모든 통합 문제는 사실상 팀 간의 커뮤니케이션을 통해 해결할 수 있다. 또 다른 협력 기반 통합 방법은 공유 커널이다. 팀은 모델의 제한된 부분을 분리해서 공동으로 함께 발전시킨다.
- 모델의 변환 로직은 스테이트리스 또는 스테이트풀이 될 수 있다. 상태를 보존하지 않는 스테이트리스 변환(stateless translation)은 수신(OHS) 또는 발신(ACL) 요청이 발행할 때 즉석에서 발생하는 반면, 스테이트풀 변환(stateful translation)은 상태 보존을 위해 데이터베이스를 사용하여 좀 더 복잡한 로직을 다룰 수 있다.
- 스테이트리스 모델 변환
- 프락시 디자인 패턴(proxy design pattern)을 구현하여 수신과 발신 요청을 삽입하고 소스 모델을 바운디드 컨텍스트의 목표 모델에 매핑한다.
- 요청 –(모델 A)–> 프락시 –(모델 B)–> 목표
- 프락시 구현은 바운디드 컨텍스트가 동기식으로 통신하는지 또는 비동기식으로 통신하는지에 따라 다르다.