본문은 도메인 주도 설계 첫걸음을 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
5장) 간단한 비즈니스 로직 분석
트랜잭션 스크립트
- 프로시저를 기반으로 시스템의 비즈니스 로직을 구성하는 패턴
- 입출력은 퍼블릭 인터페이스를 통하고 내부 구현은 프로시저로 구성된 스크립트
- 프로시저는 간단하고 쉬운 절차지향 스크립트(procedure script)로 구현
- 절차지향 스크립트? 작업을 순차적으로 수행하는 것을 의미(↔ 객체지향 언어)
- 내부적으로는 데이터베이스에 직접 접근한다
- 입출력은 퍼블릭 인터페이스를 통하고 내부 구현은 프로시저로 구성된 스크립트
- 트랜잭션을 적용해 작업이 모두 성공하거나 실패함을 보장해야 한다
- 단순함이 최고의 장점
- e.g. 원천 시스템의 데이터를 변환해서 목적 시스템에 전달 (단순 DTO Query를 생각해보자)
- 비즈니스 로직이 단순한 지원 하위 도메인에 적합
- 어댑터 or 충돌 방지 계층에도 사용 가능
- 비즈니스 로직이 복잡할 때는 사용하면 안 된다 (= 핵심 하위 도메인에는 사용하면 안 됨!)
- 비즈니스 로직의 중복
- 중복된 코드가 동기화되지 않을 때 일관성 없는 동작 발생할 수 있음
액티브 레코드
- 비즈니스 로직이 단순하지만, 자료구조가 복잡할 때 사용하는 패턴
- 데이터 접근 로직을 구현한다
- 트랜잭션 스크립트로 시스템의 비즈니스 로직을 만든다
- 액티브 레코드는 트랜잭션 스크립트와 달리 데이터베이스에 직접 접근하지 않는 대신, 트랜잭션 스크립트가 액티브 레코드 객체를 조작한다
- 간단한 CRUD 데이터 접근 방법을 제공하는 자료구조
- 비즈니스 로직이 포함될 수 있으나, CRUD 및 입력 유효성 검증과 같은 간단한 비즈니스 로직만 수행 가능
- 액티브 레코드는 데이터베이스 스키마 매핑의 복잡성을 해소한다는 것이 특징
- 보다 자세한 예시는 아래 문서를 참고하자
6장) 복잡한 비즈니스 로직 다루기
도메인 모델
- 복잡한 비즈니스 로직을 다루기 위한 패턴
- CRUD 인터페이스 대신 복잡한 상태 전환, 항상 보호해야 하는 규칙인 비즈니스 규칙과 불변성을 다룬다
- 도메인 모델 패턴은 복잡한 비즈니스 로직을 갖는 하위 도메인에만 적용되므로 이를 핵심 하위 도메인으로 가정해도 좋다
도메인 모델의 구현
- 행동(behavior)과 데이터(data)를 모두 포함하는 도메인의 객체 모델
- DDD의 전술 패턴인 애그리게이트, 밸류 오브젝트, 도메인 이벤트, 도메인 서비스는 모두 객체 모델의 구성요소
복잡성
- 모델에는 인프라 또는 기술적인 관심사를 피해야 한다 (e.g. 데이터베이스, 외부 API 호출 등)
- 결국 모델은 플레인 올드 오브젝트(plain old object)가 된다 (=POJO)
유비쿼터스 언어
- 도메인 모델의 객체가 기술적 관심사가 아닌 비즈니스 로직에 집중하게 해야 한다
- 이로써 코드에서 유비쿼터스 언어를 사용하게 하고 도메인 전문가의 멘탈 모델을 따르게 하는 효과가 있다
도메인 모델의 구성요소
1) 밸류 오브젝트
- 값에 의해 식별되는 객체
data clss Color(
val red: Int,
val green: Int,
val blue: Int
)
- 불변 객체이다
- 필드 중 하나가 바뀌면 개념적으로 새로운 인스턴스가 생성되는 것이다
- 밸류 오브젝트를 사용하지 않고 원시 데이터 타입에 전적으로 의존해서 비즈니스 도메인을 표현하는 것은 원시 집착 코드 징후(primitive obsession code smell)이다
- 유효성 검사 로직이 중복됨
- 값이 사용되기 전에 유효성 검사 로직을 호출하게 하기 어려움
- 유지보수가 어려움
- https://refactoring.guru/ko/smells/primitive-obsession 참고
- 밸류 오브젝트의 장점
- 명료성 향상 (변수의 의도가 명확해짐)
- 값을 할당하기 전에 유효성 검사를 할 필요가 없어짐
- 응집된 로직은 한 곳에서 구현되고 쉽게 테스트할 수 있음
- 코드에서 비즈니스 도메인의 개념을 표현 (코드에서 유비쿼터스 언어를 사용하므로!!)
- 사용하는 경우
- 가능한 모든 경우에 사용하는 게 좋음
- 다른 객체의 속성을 표현하는 도메인 요소에 밸류 오브젝트를 사용하라
2) 엔티티
- 식별 필드에 의해 식별되는 객체
- 식별 필드는 각 엔티티 인스턴스마다 고유해야 한다
- 엔티티의 식별 필드의 값은 엔티티의 생애주기 내내 불변이어야 한다
class Person(
val personId: Id, // 식별 필드
val name: Name
)
- 밸류 오브젝트와 반대로 불변이 아니라 변할 것으로 예상됨
- 앞서 소개한 밸류 오브젝트는 엔티티의 속성을 설명하는 것
- 엔티티는 단독으로 구현되지 않고, 애그리게이트 패턴의 컨텍스트에서만 구현된다
3) 애그리게이트
- 엔티티 + α
- 애그리게이트는 기본적으로 엔티티
- 따라서 명시적인 식별 필드가 필요하고, 상태가 변할 것으로 예상됨
- 그러나 단순한 엔티티 그 이상의 존재다 (데이터의 일관성을 보호한다!)
- 데이터의 일관성을 보호하는 것이 존재 목적
- 데이터의 일관성은 애그리게이트의 비즈니스 로직을 통해서만 애그리게이트의 상태를 변경하게 해야 강화됨
- 애그리게이트의 퍼블릭 인터페이스는 입력값의 유효성을 검사하고 관련된 모든 비즈니스 규칙과 불변성을 강화하는 것을 담당
- 퍼블릭 인터페이스로 노출된 상태 변경 메서드는 커맨드라고 함
- 일반적인 퍼블릭 메서드로 만들거나 커맨드 객체를 별도로 메서드의 아규먼트로 넘겨서 구현할 수 있음
- 외부 객체는 애그리게이트 내의 데이터 필드를 읽을 수만 있음
- 결국 애그리거트는 일관성을 강화하는 경계가 됨
- 트랜잭션 경계로서의 애그리게이트
- 애그리게이트의 상태는 자신의 비즈니스 로직을 통해서만 수정될 수 있으므로 애그리게이트는 트랜잭션 경계의 역할을 함
- 애그리게이트 상태 변경은 원자적인 단일 오퍼레이션으로 트랜잭션 처리
- 애그리게이트의 상태가 수정되면 모든 변경이 커밋되거나 모두 롤백됨
- 다중 애그리게이트 트랜잭션은 원칙적으로 지원되지 않는다
- 애그리게이트간 참조
- 애그리게이트가 너무 커지면 성능과 확장 문제가 생길 수 있음 (같은 트랜잭션 경계를 공유하므로)
- 따라서 비즈니스 로직에 따라 강력한 일관성이 필요한 정보만 애그리게이트에 포함되면 됨
- 애그리게이트를 가능한 작게 유지하고 강력하게 일관적으로 상태를 유지할 필요가 있는 객체만 포함
- 애그리게이트 외부의 객체 참조는 ID를 사용한다
- 해당 객체가 애그리게이트 경계에 속하지 않음을 명확히 하고, 각 애그리게이트가 자신의 트랜잭션 경계를 갖게 보장함
- 애그리게이트 루트
- 애그리게이트는 엔티티의 계층 구조를 대표한다
- 따라서 그중 하나만 애그리게이트의 퍼블릭 인터페이스, 즉 애그리게이트 루트로 지정돼야 한다
- 도메인 이벤트
- 비즈니스 도메인에서 일어나는 중요한 이벤트를 설명하는 메시지
- 이미 발생된 것이므로 과거형으로 명명
- 비즈니스 도메인에서 일어난 일을 설명하고, 이벤트와 관련된 모든 필요한 데이터를 제공하는 것이 목적
4) 도메인 서비스
- 비즈니스 로직을 구현한 상태가 없는 객체
- 밸류 오브젝트에도 속하지 않거나 복수의 애그리게이트에 관련된 비즈니스 로직을 다루게 될 때 사용하는 패턴
- 여러 애그리게이트의 데이터를 읽는 것이 필요한 계산 로직을 구현하는 것을 도와줌
7장) 시간 차원의 모델링
이벤트 소싱 도메인 모델 패턴
- 데이터 모델에 시간 차원을 도입
- 애그리게이트 수명주기의 모든 변경사항을 분석하는 이벤트를 유지
이벤트 소싱 애그리게이트의 작업 단계
- 애그리게이트의 도메인 이벤트를 로드
- 분석을 목적으로 프로젝션해서 상태 표현을 재구성
- 애그리게이트 명령을 실행하여 비즈니스 로직을 수행하고, 결과적으로 새로운 도메인 이벤트를 생성
- 새 도메인 이벤트를 이벤트 스토어에 커밋
- 이벤트 스토어는 추가만 가능한 저장소 (수정 or 삭제 X)
- 특정 비즈니스 엔티티에 속한 모든 이벤트를 가져오고 이벤트를 추가해야 함
이벤트 소싱 도메인 모델 패턴의 장점
- 애그리게이트의 모든 과거 상태를 추적할 수 있다
- 소싱한 이벤트를 유의미한 모델로 자유롭게 변환할 수 있다
- 영속적인 도메인 이벤트는 애그리게이트 상태에서 발생한 모든 것에 대한 강력하게 일관된 감사 로그(audit log)를 나타낸다
- 고급 낙관적 동시성 제어를 사용할 수 있다
- 읽기 데이터가 기록되는 동안 다른 프로세스에 의해 덮어 쓰어지는 경우를 방지한다
이벤트 소싱 도메인 모델 패턴의 단점
- 러닝 커브가 높다
- 이벤트 소싱 모델을 변화시키는 것이 어렵다
- 아키텍처의 복잡도가 높아진다
8장) 아키텍처 패턴
- 여기서 소개하는 아키텍처 패턴들은 시스템 전체의 구성 원칙이나 전체 바운디드 컨텍스트를 위한 고수준의 아키텍처 패턴이 아니다
- 바운디드 컨텍스트 내의 여러 하위 도메인에서 필요에 따라 다양한 패턴들을 적용할 수 있다 (수직적 분할)
계층형 아키텍처
- 기본적으로 프레젠테이션 계층, (서비스 계층), 비즈니스 로직 계층, 데이터 접근 계층으로 나눠짐
- 탑-다운 구조의 의존 관계를 지님
- 세부 구조 (위에서부터 고수준)
- 프레젠테이션 계층
- 비즈니스 로직 계층
- 데이터 접근 계층
프레젠테이션 계층 (= 사용자 인터페이스 계층)
- 기본적으로 그래픽 인터페이스를 나타낸다 (e.g. 웹 인터페이스, 데스크톱 인터페이스)
- 프로그램의 동작을 촉발하는 모든 동기식 또는 비동기식 수단과 같은 좀 더 광범위한 범주를 포함
서비스 계층 (= 애플리케이션 계층)
- 계층형 아키텐처 패턴을 확정해서 서비스 계층을 추가할 수 있다
- 논리적 경계
- 비즈니스 로직 계층으로의 관문 역할
- 비즈니스 로직을 오케스트레이션
- 하부 계층을 조율하는 데 필요한 것들을 감싸서 퍼블릭 인터페이스의 메서드에 상응하는 인터페이스로 노출한다
- 비즈니스 로직 패턴에서 외부 조율을 해야 할 경우에만 서비스 계층이 필요하다
- 트랜잭션 스크립트는 이미 퍼블릭 인터페이스를 통해서 비즈니스 로직이 구성되어 있다
- 따라서 트랜잭션 스크립트 패턴에서는 서비스 계층 사용이 적절치 않고, 액티브 레코드 패턴에서는 적절하다
비즈니스 로직 계층 (= 도메인 계층 = 모델 계층)
- 비즈니스 의사결정을 구현하는 계층
- 액티브 레코드 또는 도메인 모델과 같은 비즈니스 로직 패턴을 이 계층에서 구현
데이터 접근 계층 (= 인프라스트럭처 계층)
- 시스템의 데이터베이스
- 영속성 메커니즘에 접근할 수 있는 계층
- 단순 RDB 외에도 더 포괄적인 내용이 포함된다
- e.g. NoSQL, 메시지 버스, 외부 API
포트와 어댑터 아키텍처 (= 헥사고날 아키텍처, 클린 아키텍처)
- 계층형 아키텍처의 단점을 해결하고 좀 더 복잡한 비즈니스 로직을 구현하는 데 적합한 패턴
- 모든 기술적 관심사로부터 비즈니스 로직을 분리하는 것이 목적
- 도메인 모델 패턴을 사용하여 구현한 비즈니스 로직에 매우 적합하다
- 프레젠테이션 계층과 데이터 접근 계층을 '인프라스트럭처 계층'으로 통합했다
- 세부 구조 (위에서부터 고수준)
- 비즈니스 로직 계층
- 애플리케이션 계층 (= 서비스 계층 = 유스케이스 계층)
- 인프라스트럭처 계층 (= 도메인 계층 = 핵심 계층)
의존성 역전 원칙
- 기존 계층형 아키텍처는 의존성 역전 원칙(DIP)를 위반한다
- 상위 수준 모듈이 하위 수준 모듈을 의존한다
- 포트와 어댑터 아키텍처는 의존성 역전 원칙이 반영되었다
- 비즈니스 로직 계층(상위 수준 모듈)이 인프라스트럭처 계층이 구현할 '포트'를 정의한다 (비즈니스 로직 계층 → 인터페이스 정의)
- 인프라스트럭처 계층은 '어댑터'를 구현한다 (인프라스트럭처 계층 → 인터페이스 구현)
- 퍼블릭 인터페이스를 위한 관문으로 애플리케이션 계층이 중간에 추가된다
CQRS(command-query responsibility segregation)
- 포트와 어댑터와 동일하게 비즈니스 로직과 인프라스트럭처 관심사에 기반한다
- 그러나 포트와 어댑터 아키텍처와 다르게 시스템의 데이터를 관리하는 방법이 다르다
- CQRS는 시스템 모델의 책임을 분리시키는 패턴이다
- 도메인 모델 패턴과 이벤트 소싱 도메인 모델에 모두 적합하다
- 또한, 인프라스트럭처 관점에선 다양한 종류의 데이터베이스의 장점을 활용할 수 있다
커맨드 실행 모델
- 시스템의 상태를 수정하는 오퍼레이션(시스템 커맨드)을 전담으로 수행하는 단일 모델
- 비즈니스 로직을 구현하고 규칙을 검사하며 불변성을 강화하는 데 사용됨
- 강력한 일관성을 데이터를 표현하는 유일한 모델
- 일관적 상태를 읽을 수 있어야 한다
- 낙관적 동시성을 지원해야 한다
읽기 모델(프로젝션)
- 사용자에게 데이터를 보여주거나 다른 시스템에 정보를 제공하기 위해 필요한 만큼 정의하는 모델
- RDB의 머터리얼라이즈 뷰(materialize view)의 개념과 유사하다
- 읽기 모델은 읽기 전용이므로 읽기 모델의 데이터를 직접 수정하는 것은 불가능하다
프로젝션의 생성 방식
- 프로젝션을 생성하는 데엔 동기식 프로젝션과 비동기식 프로젝션이라는 두 가지 방법이 존재한다
- 동기식 프로젝션
- 프로젝션 엔진이 마지막에 처리한 체크포인트 이후에 추가되거나 갱신된 레코드를 조회한다
- 조회된 데이터를 이용해서 읽기 모델을 재생성/갱신한다
- 다시 처리 체크포인트를 기록한다
- 정리하자면 상태 변경 -> 읽기 모델 반영 -> 체크 포인트 기록이라는 흐름으로 프로젝션을 생성한다
- 비동기식 프로젝션
- 커맨드 실행 모델이 모든 변경사항을 메시지 버스에 발행한다
- 프로젝션 엔진은 이를 구독하고 읽기 모델에 반영한다
- 가능하면 동기식 프로젝션 방식을 구현하고, 선택적으로 비동기식 프로젝션 방식을 추가하는 것을 권장한다
- 비동기식 프로젝션 방식은 확장성과 성능적인 측면에서 유리하지만, 분산 컴퓨팅에서 문제가 발생하기 더 쉽다
- 메시지의 순서가 꼬이거나 중복 처리가 일어나면 일관성 없는 데이터가 프로젝션 될 수 있다
모델 분리
- CQRS에서 데이터 조회는 읽기 모델을 통해서만 이뤄지는 것이 아니다
- 커맨드가 데이터를 반환함으로써 동작에 대한 피드백을 받을 수 있다 (e.g. 실패/성공에 대한 사유)
- 사용자 입장에서 반환값을 다음 워크플로우에 사용할 수 있다
- 데이터가 궁극적으로 일관성을 갖는 프로젝션의 경우에는 데이터에 대한 즉각적인 갱신은 기대할 수 없다
'책 > 도메인 주도 설계 첫걸음' 카테고리의 다른 글
[도메인 주도 설계 첫걸음] Part 4. 다른 방법론 및 패턴과의 관계 (1) | 2023.10.09 |
---|---|
[도메인 주도 설계 첫걸음] Part 3. 도메인 주도 설계 적용 실무 (0) | 2023.10.08 |
[도메인 주도 설계 첫걸음] Part 1. 비즈니스 도메인 분석하기 (0) | 2023.08.27 |