도메인 모델 시작
도메인
- 도메인 : 소프트웨어로 해결하고자 하는 문제 영역
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
- 예를 들어, 온라인 서점 도메인은 다시 주문, 배송 등의 도메인으로 나눌 수 있다.
도메인 모델
- 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
- 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
- 모델의 각 구성요소는 특정 도메인을 한정할 때 비로소 의미가 완전해지기 때문에, 각 하위 도메인마다 별도로 모델을 만들어야 한다. 이는 주문 도메인 모델과 배송 도메인 모델은 따로 만들어야 한다.
도메인 모델 도출
- 도메인 계층은 도메인읜 핵심 규칙을 구현한다.
- 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바귀거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
- 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
엔티티와 밸류
- 도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다.
엔티티
- 엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다.
- 엔티티를 생성하고 엔티티의 속성을 바꾸고 삭제할 때까지 식별자는 유지된다.
엔티티의 식별자 생성
- 특정 규칙에 따라 생성
- UUID 사용
- 값을 직접 입력
- 일련번호 사용 (시퀀스나 DB의 자동 증가 칼럼 사용)
밸류 타입
- 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
- 장점으로 밸류 타입을 위한 기능을 추가할 수 있다.
- 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.
- 이유는 불변 타입을 사용하면 보다 안전한 코드를 작성할 수 있기 때문이다.
엔티티 식별자와 밸류 타입
- 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.
- 식별자를 위한 밸류 타입을 만들면 타입 자체로 의미를 명확히 할 수 있다.
도메인 용어
- 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 도메인에서 사용하는 용어를 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.
아키텍처 개요
네 개의 영역
- 아키텍처를 설계할 때 출현하는 전형적인 영역이 '표현', '응용', '도메인', '인프라스트럭처' 영역이다.
- 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환해서 전송한다.
- 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현한다.
- 응용 서비스는 로젝을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
- 도메인 영역은 핵심 로직이 담긴 도메인 모델을 구현한다.
- 인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다.
- RDBMS 연동 처리, 메시징 큐에 메시지 전송 또는 수신, SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP 클라이언트를 이용해서 REST API를 호출하는 것도 처리한다.
계층 구조 아키텍처
- 전체적인 아키텍처는 다음 그림의 계층 구조를 따른다.
- 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다.
- 하지만, 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다.
- 이렇게 인프라 스트럭처에 의존하면 '테스트 어려움' 과 '기능 확장의 어려움' 이라는 두 가지 문제가 발생한다.
DIP
- DIP는 인터페이스를 이용해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.
- DIP를 적용하면 구현 기술을 변경하더라도 클라이언트 코드는 수정할 필요가 없다
- 단지, 사용할 저수준 구현 객체만 교체해주면 된다.
- 테스트를 할 때도 대용 객체를 사용해서 진행할 수 있다.
- 실제 구현 대신, Mock과 같은 대용 객체를 사용한다.
도메인 영역의 주요 구성요소
요소 | 설명 |
엔티티 | 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. |
밸류 | 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용된다. |
애그리거트 | 애그리거트는 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. |
리포지터리 | 도메인 모델의 영속성을 처리한다. |
도메인 서비스 | 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. |
엔티티와 밸류
- 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다.
- 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다.
애그리거트
- 애그리거트는 관련 객체를 하나로 묶은 군집이다.
- 예를 들어, 주문이라는 도메인 개념은 '주문', '배송지 정보', '주문자', '주문 목록', '결제 금액' 의 하위 모델로 구성되는데 이때 이 하위 모델들을 하나로 묶어서 '주문'이라는 상위 개념으로 표현할 수 있다.
- 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다.
- 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.
리포지터리
- 도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 도메인 객체를 보관해야 한다.
- 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
요청 처리 흐름
- 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 이때, 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
- 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 기능 구현에 필요한 도메인 객체를 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다.
인프라스트럭처 개요
- 인프라슽트럭츠는 표현 영여가, 응용 영역, 도메인 영역을 지원한다.
- 영속성 처리, 트랜잭션, SMTP 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다.
애그리거트
애그리거트
- 상위 수준 개념을 이용해서 전체 모델을 정리하면 전반적인 관계를 이해하는 데 도움이 된다.
- 반대로, 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다.
- 상위 수준에서 모델이 어떻게 엮여 있는지 알아야 전체 모델을 망가뜨리지 않으면서 추가 요구사항을 모델에 반영할 수 있다.
- 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들어 상위 수준에서 모델을 조망할 수 있는 방법이 애그리거트이다.
- 애그리거트는 모델을 이해하는 데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준이 된다.
- 애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
애그리거트 루트
- 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다.
- 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 경계를 관리할 주체가 필요한데 이 책임을 지는 것이 바로 애그리거트의 루트 엔티티이다.
도메인 규칙과 일관성
- 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다.
- 애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안 된다.
- 이는 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성이 깨지게 된다.
- 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 다음의 두 가지를 습관적으로 적용해야 한다.
- set 메서드를 public 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
애그리거트 루트의 기능 구현
- 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
- 애그리거트 루트는 구성요소의 상태를 참조하거나 기능 실행을 위임한다.
트랜잭션 범위
- 트랜잭션 범위는 작을수록 좋다.
- 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.
- 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.
리포지터리와 애그리거트
- 애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
ID를 이용한 애그리거트 참조
- 필드를 이용해서 다른 애그리거트를 직접 참조하는 것은 개발자에게 구현의 편리함을 제공한다.
- 하지만, 필드를 이용한 참조는 다음의 문제를 야기할 수 있다.
- 편한 탐색 오용
- 성능에 대한 고민
- 확장 어려움
- 편한 탐색 오용 : 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 수정하고자 하는 유혹에 빠지기 쉽다.
- 성능에 대한 고민 : JPA를 사용할 경우 참조한 객체를 지연 로딩과 즉시 로딩의 두 가지 방식으로 로딩할 수 있다. 이는 애그리거트가 어떤 기능을 사용하느냐에 따라 로딩 방식이 달라지고, 성능도 달라진다.
- 확장 어려움 : 트래픽이 늘어서 하위 도메인별로 시스템을 분리할 경우, 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다.
- 이런 세 가지 문제를 완화할 때 사용할 수 있는 것이 ID를 이용한 참조이다.
- ID 참조를 사용하면 한 애그리거트에 속한 객체끼리만 참조로 연결된다
- ID를 이용한 참조 방식은 복잡도를 낮추는 것과 함께 애그리거트 별로 다른 구현 기술을 사용하는 것도 가능해진다.
응용 서비스와 표현 영역
표현 영역과 응용 영역
- 표현 영역은 사용자의 요청을 해석한다.
- URL, 요청 파라미터, 헤더 등을 이용해서 사용자가 어떤 기능을 실행하고 싶어하는지 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
- 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
- 실제 사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스이다.
응용 서비스의 역할
- 응용 서비스는 클라이언트가 요청한 기능을 실행한다.
- 응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같은 단순한 형태를 갖는다.
- 리포지터리에서 애그리거트를 구한다.
- 애그리거트의 도메인 기능을 실행한다.
- 결과를 리턴한다.
- 응용 서비스가 이것보다 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다.
- 응용 서비스의 또다른 주된 역할은 트랜잭션 처리이다.
도메인 로직 넣지 않기
- 도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다.
- 응집성이 떨어진다. 도메인 로직을 파악하기 어려워진다.
- 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
응용 서비스의 구현
응용 서비스의 크기
- 응용 서비스는 보통 다음의 두 가지 방법 중 한가지 방식으로 구현한다.
- 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
- 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
메서드 파라미터와 값 리턴
- 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다.
도메인 이벤트 처리
- 응용 서비스의 역할 중 하나는 도메인 영역에서 발생시킨 이벤트를 처리하는 것이다.
- 여기서 이벤트는 도메인에서 발생하는 상태 변경을 의미한다.
- 도메인에서 이벤트를 발생시키면 응용 서비스는 이벤트를 받아서 알맞은 후처리를 할 수 있다.
표현 영역
- 표현 영역의 책임은 크게 다음과 같다.
- 사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어한다.
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
- 사용자의 세션을 관리한다.
값 검증
- 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
- 표현 영역은 잘못된 값이 존재하면 이를 사용자에게 알려주고 값을 다시 입력받아야 한다.
- 표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 아이디 중복 여부와 같은 논리적 오류만 검사하면 된다.
- 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증한다.
- 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류를 검증한다.
조회 전용 기능과 응용 서비스
- 서비스에서 조회 외에 추가적인 로직이 없다면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 긴능을 사용해도 된다.
도메인 서비스
여러 애그리거트가 필요한 기능
- 한 애그리거트에 넣기에 애매한 도메인 기능을 특정 애그리거트에서 억지로 구현하면 안 된다.
도메인 서비스
- 한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다. 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
- 도메인 서비스는 상태 없이 로직만 구현한다. 도메인 서비스를 구현하는 데 필요한 상태는 애그리거트나 다른 방법으로 전달받는다.
도메인 모델과 BOUNDED CONTEXT
도메인 모델과 경계
- 재고 관리에서의 상품, 주문에서의 상품, 배송에서의 상품은 이름만 같지 실제로 의미하는 것은 다르다.
- 올바른 도메인 모델을 개발하려면 하위 도메인마다 모델을 만들어야 한다.
- 각 모델은 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야 한다.
BOUNDED CONTEXT
- BOUNDED CONTEXT는 모델의 경계를 결정하며 한 개의 BOUNDED CONTEXT는 논리적으로 한 개의 모델을 갖는다.
- 이상적으로는 하위 도메인과 BOUNDED CONTEXT가 일대일 관계를 가지면 좋겠지만 현실은 다르다.
- 만약, 여러 하위 도메인을 하나의 BOUNDED CONTEXT에서 개발할 때 주의할 점은 하위 도메인의 모델이 뒤섞이지 않도록 하는 것이다.
BOUNDED CONTEXT 구현
- BOUNDED CONTEXT는 도메인 모델뿐만 아니라 도메인 기능을 사용자에게 제공하는 데 필요한 표현 영역, 응용 서비스, 인프라 영역 등을 모두 포함한다.
- 모든 BOUNDED CONTEXT를 반드시 DDD로 개발할 필요는 없다. 복잡한 도메인 로직이 없다면 CRUD방식으로 구현해도 된다.
- 즉, DAO와 데이터 중심의 밸류 객체를 이용해서 구현해도 문제가 없다.
이벤트
시스템 간 강결합의 문제
- 하나의 요청 로직에서 둘 이상의 BOUNDED CONTEXT가 뒤섞이면 문제가 발생한다.
- 외부 서비스가 정상이 아닐 경우 트랜잭션 처리가 애매해진다.
- 외부 시스템의 응답 시간이 길어지면 성능에 문제가 생긴다.
- 이벤트를 사용하면 이런 강한 결합을 없앨 수 있다.
이벤트 개요
- 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.
- 이벤트가 발생하면 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.
이벤트 관련 구성요소
- 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다. 이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다.
- 이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
- 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.
- 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처이다.
- 이벤트 생성주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다.
- 이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.
이벤트의 구성
- 이벤트는 발생한 이벤트에 대한 정보를 담는다.
- 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현
- 이벤트 발생 시간
- 추가 데이터 : 주문 번호, 신규 배송지 정보 등 이벤트와 관련된 정보
- 이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 과거 시제를 사용한다.
이벤트 용도
- 첫 번째 용도는 트리거이다.
- 도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
- 이벤트의 두 번째 용도는 서로 다른 시스템 간의 데이터 동기화이다.
이벤트 장점
- 이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
- 이벤트 핸들러를 사용하면 기능 확장도 용이하다.
CQRS
단일 모델의 단점
- 여러 애그리거트가 연관된 조회를 하면 즉시 로딩이나 지연 로딩의 고민이 생긴다.
- 이런 고민이 발생하는 이유는 시스템의 상태를 변경할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다.
- 복잡도를 낮추는 간단한 방법은 상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것이다.
CQRS
- 시스템이 제공하는 기능은 크게 상태를 변경하는 기능과 상태 정보를 조회하는 기능이다.
- 단일 모델로 두 종류의 기능을 구현하면 모델이 불필요하게 복잡해진다.
- CQRS는 상태를 변경하는 명령을 위한 모델과 상태를 제공하는 조회를 위한 모델을 분리하는 패턴이다.
- CQRS는 복잡한 도메인에 적합하다.
- CQRS를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다.
- 단순히 데이터를 읽어와 조회하는 기능은 응용 로직이 복잡하지 않기 때문에 컨트롤러에서 바로 DAO를 실행해도 무방하다.
'독서' 카테고리의 다른 글
'함께 자라기' 를 읽고 (1) | 2021.08.12 |
---|