유스케이스 태스크를 조정
트랜잭션, 보안 권한 부여 담당
애플리케이션을 핵심 도메인 모델과 상호 교류하며 이를 지원하기 위해 잘 조합된 컴포넌트의 집합을 의미하기 위해 사용하고 있다.
이는 일반적으로 도메인 모델 그 자체와 사용자 인터페이스, 내부적으로 사용되는 애플리케이션 서비스, 인프라적 컴포넌트를 뜻한다.
이 각각의 영역에 어떤 것들이 들어가는지는 애플리케이션마다 다르며, 사용하는 아키텍처가 무엇인지에 따라서도 달라진다.
그림 14.1 특정한 한 가지 아키텍처에 국한되지 않은 주요 애플리케이션 영역. 여전히 각기 다른 영역의 추상화에 의존적인 인프라의 DIP를 강조한다.
ui.Controller -구현-> infra.TomcatController -위임-> application.Service -구현-> infra.TransactionalService -위임-> domain.Repository -구현-> infra.JpaRepository -의존-> infra.Mysql
필자는 UI -> Appcliation -> Infra 순서로 설명해 나간다. (Top-Down 방식)
사용자 인터페이스
UI 종류
Web 1.0 렌더링 되는 HTML
Web 2.0 DHTML + Ajax
데스크톱 애플리케이션 + HTTP 통신
도메인 객체의 렌더링
그림 14.2 조회 시에는 복수 의 애그리게잇. 명령 시에는 한 개의 애그리게잇
애그리게잇 인스턴스로부터 데이터 전송 객체를 렌더링하기
DTO + Assembler
개인적으로 가장 선호하는 방식
애그리게잇 내부 상태를 발행하기 위해 중재자를 사용하자
중재자 패턴
개인적으로 비추 도메인 모델이 (약하긴 하지만) UI에 의존하게 됨
도메인 페이로드 객체로부터 애그리게잇 인스턴스를 렌더링하라
도메인 객체를 내부에 담고 있는 DPO(Domain Payload Object)로 렌더링 하는 방식
지연로딩 이슈가 있기 때문에 OSIV와 같은 기술과 함께 쓰길 권장한다.
애그리게잇 인스턴스의 상태 표현
View Model? Presentation Model?
DTO와 차이점을 모르겠다. 상태를 표현한다는 것으로 보아 불변이 가능한 정도 차이인 것인가?
유스케이스 최적 리파지토리 쿼리
특정 유스케이스에 특화된 쿼리 메소드 제공.
CQRS와 구분하자.
다수의 개별 클라이언트 처리하기
이건 SKIP 하자. 최근의 MVC 프레임워크에서 지원하므로 Application의 책임이 아니라고 본다.
변환 어댑터와 사용자 편집의 처리
Adapter + Command (Or Presentation Model)
주로 명령(편집, 수정) 시에 사용하는 방식
애플리케이션 서비스
애플리케이션 서비스를 얇게 유지(파사드)
도메인 모델로 위임
애플리케이션 서비스 예제
원시타입의 나열 VS 인자 (커맨드) 객체
원시타입이 너무 많이 나열되면 그것은 안티패턴이라고 보고 가능하면 인자(Or 커맨드)로 캡슐화 하는 것이 좋아 보인다.
단순 인자이며 명령을 캡슐화 하지 못하는데 커맨드 패턴이라고 부르는 것이 어색해 보인다.
package com . saasovation . identityaccess . application ;
class TenantIdentityService {
@Transactional // 트랜잭션과 보안 처리
@PreAuthorize ( "hasRole('SubscriberRepresentative')" )
public void activeTenant ( TenantId tenantId ) {
// 도메인 모델로 위임
this . nonNullTenant ( tenant ). activate ();
}
@Transactional ( readOnly = true )
public Tenant nonNullTenant ( TenantId tenantId ) {
Tenant tenant = this . tenant ( tenantId );
if ( tenant == null ) {
throw new IllegalArgumentException ( "Tenant does not exists" );
}
return tenant ;
}
}
결합이 분리된 서비스 출력
@Override
@Transactional ( readOnly = true )
public void findTenant ( TenantId tenantId ) {
Tenant teannt =
this . tenantRepository . tenantOfId ( tenantId )
this . tenantIdentityOutputPort (). write ( tenant );
}
최근 추세는 어쨋든 사용자에게 출력에 대한 책임은 가능한 UI가 하는 것이 좋다고 본다.
이는 애플리케이션의 책임이 아닌 것 같다.
그러한 것은 컴포넌트로 추상화 해서 UI 모듈에 있는 것이 더 어울리는 것 같다. 개인적으로 출력은 반환 값이 있는 것이 더 직관적으로 보인다.
여러 바운디드 컨텍스트 묶기
그림 14.3 UI에서 다수의 모델을 구성해야만 할 때가 있다. 이 그림에서는 하나의 애플리케이션 계층을 사용해서 세 개의 모델을 구성한다.
애플리케이션 계층이 유스케이스 를 관리 - UI 계층이 아니다!!
제품, 토론, 리뷰 컨텍스트가 분리 VS 하나의 컨텍스트로 통합 > 결론은 모델 정제!!
개인적으로 이러한 상황에서는 UI에서 각각 쿼리 해서 조합하던가, API-GW에서 조합하는 것이 좋다고 본다.
물론 특정 컨텍스트 내에서 조합해야 한다면 애플리케이션 서비스 가 가장 어울리는 장소인 것 같다.
인프라
그림 14.4 애플리케이션 서비스는 도메인 모델의 리파지토리 인터페이스에 의존적이지만, 인프라의 구현 클래스를 사용한다. 패키지는 넓은 범위의 책임을 캡슐화 한다
엔터프라이즈 컴포넌트 컨테이너
EJB VS Spring
마무리
도메인 모델 -> UI 모델
사용자 입력 -> 도메인 모델
애플리케이션 서비스의 책임
DIP로 도메인 모델과 기술(인프라) 분리 및 유연성 확보
IDDD 14장. 애플리케이션 was originally published by MJ at DevOOOOOOOOP on June 19, 2018.
source : http://redutan.github.io/2018/06/19/IDDD-chapter14
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
특성만 신경 쓴다면 이를 값 객체로 분리하라. 특성의 의미를 표현하고 기능도 부여하자. 불변성. 식별자는 필요 없고, 설계의 복잡성을 줄여준다.
값의 특징
개념을 값으로
측정, 수량화, 설명
불변성(no side-effect)
개념적 전체
변경 대신 대치
등가성(value equality:동등성)
유비쿼터스 언어로써 표현하는 값이어야함
측정, 수량화, 설명
날짜와 시간, 나이, 통화, 주소, 도형, …
불변성
No Setter
하지만 완벽하진 않다. 방어 복사가 필요할 수 있음 (final 일지라도 불변은 깨질 수 있다.)
개념적 전체
주소 = 우편번호 + 기본주소 + 상세주소
돈 = 500(수량) + 달러(통화)
Bad Case
class ThingOfWorth { // 가치 있는 것
private String name ;
private BigDecimal amount ;
private String currency ;
}
Good Case
class ThingOfWorth { // 가치 있는 것
private ThingName name ; // !!
private MonetaryValue worth ; // 가치
}
@Value
class MonetraryValue {
private BigDecimal amount ;
private Currency currency ;
}
ThingName 으로 값 객체로 만들어서 name의 기능을 중앙화 시킬 수 있다. (도메인 로직을 안으로 품을 수 있음)
대체성
값은 변경이 불가능하므로 값 참조를 다른 값으로 바꾸는 것을 말함
FullName name = new FullName ( "Myeongju" , "Jung" );
// ...
name = new FullName ( "Jiwon" , "M" , "Jung" );
값 등가성
equals(), hasoCode()
엔터티로 설계된 개념이 식별자가 필요하지 않다면 값 객체 로 모델링하자.
부작용이 없는 행동
만약 값객체에 행위가 없다면, 좋은 설계인지 의심하라
Getter : No side-effect
Setter : With side-effect
FullName name = new FullName ( "Myeongju" , "Jung" );
// ...
name = name . withMiddleInitial ( "M" );
FullName withMiddleInitial ( String middleName ) {
return new FullName ( this . firstName , middleName , this . lastName );
}
값 객체의 매개변수로 값만 전달하자.
만약 엔터티와 같은 가변성을 가지는 인자를 받는다면 부작용이 발생할 가능성 이 생긴다.
아니면 Getter만 제공하는 추상화된 인자를 받는 것도 좋은 방법이다. > Entity에 Getter만 있는 인터페이스를 Mixin
기본 언어 값 타입에는 도메인에 맞춘 부작용이 없는 함수를 할당할 수 없다
도메인적인 표현이 기본 언어 함수에서 제공할 수 있을리가 없다.
미니멀리즘으로 통합하기
ACL 내에서는 조회용으로써 일부 특성과 행위가 요구된다.
즉 Entity 전체가 필요한(순응자나 공유모델) 것이 아니라, 해당 컨택스트 내에서 어울리는 타입으로써 값 객체가 좋은 선택
값으로 표현되는 표준 타입
표준 타입에는 Enum을 쓰는 것이 좋다
잘 이해가 안된다 ㅠㅠ
값 객체의 테스트
클라이언트 관점에서 값 객체를 보는 것은 중요하다.
불변성을 테스트 해야 한다.
테스트를 통해서 도메인적 표현을 확인할 수 있어야 한다.
구현
불변, 불변, 불변 (No Setter)
equals() , hashCode() , toString()
JPA를 위해서 빈 생성자는 필수 (PACKAGE 접근제어로 생성)
보호절을 통한 불변식 강제
값 객체의 저장
데이터 모델 누수의 부정적 영향을 거부하라
도메인 모델을 위해서 데이터 모델을 설계해야한다.
ORM 구현
오래된 내용이라서 SKIP. 이젠 Spring Data Jpa 를 사용하자.
마무리
값 객체 특징, 사용, 구현
IDDD 6장. 값 객체 was originally published by MJ at DevOOOOOOOOP on May 15, 2018.
source : http://redutan.github.io/2018/05/15/IDDD-chapter06
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
잘 알려진 전역의 인터페이스를 통한 액세스를 설정하자.
객체를 추가하거나 제거하는 메소드를 제공하자…
일정 조건에 부합되는 특성을 갖는 객체를 선택해 완전히 인스턴스화된 객체나 여러 객체의 컬렉션으로 반환하는 메소드를 제공하자…
애그리게잇에 대해서만 리파지토리를 제공하자.. [Evans]
애그리게잇 : 리파지토리 = 1(..N):1
컬렉션 지향 리파지토리
package java . util ;
public interface Collection {
public boolean add ( Object o );
public boolean addAll ( Collection c );
public boolean remove ( Object o );
public boolean removeAll ( Collection c );
}
같은 인스턴스를 두번 추가되도록 허용해서는 안된다.
재저장 을 할 필요가 없다.
그저 객체의 상태를 변경시키면 자동으로 저장될 뿐이다.
결국 Set 처럼 행동해야 한다.
재저장 할 필요가 없다면 객체의 상태를 추적해야한다.
영속성 매커니즘의 암시적 변경 추적 기법
암시적 읽기 시 복사(Copy-on-Read) : 읽을 때 복사본을 만들어 두고 복사본과 비교해서 변경된 것이 있으면 Commit 시킨다.
암시적 쓰기 시 복사(Copy-on-Write) : 프록시로 감싸두고 Dirty(상태 변경이 되면)인 경우 Commit 시킨다.
하이버네이트 구현
package com . saasovation . collaboration . domain . model . calendar ; //!!
interface CalendarEntryRepository {
void add ( CalendarEntry calendarEntry );
void addAll ( Collection calendarEntries );
void remove ( CalendarEntry calendarEntry );
void removeAll ( Collection calendarEntries );
CalendarEntry calendarEntryOfId ( Tenant tenant , CalendarEntriyId id );
Collection calendarEntriesOfCalendar (
Tenant tenant , CalendarId calendarId );
Collection overlappingCalendarEntries (
Tenant tenant , CalendarId calendarId , TimeSpan timeSpan );
CalendarEntryId nextIdentity ();
}
package 는 도메인 모델과 함께한다.
물리적 삭제 VS 논리적 삭제
package com . saasovation . collaboration . infrastructure . persistence ;
public class HibernateCalendarEntryRepository
implements CalendarEntryRepository {
private final SpringHibernateSessionProvider sessionProvider ;
public HibernateCalendarEntryRepository (
SpringHibernateSessionProvider sessionProvider ) {
this . sessionProvider = sessionProvider ;
}
private org . hibernate . Session session () {
return this . sessionProvider . session ();
}
@Override
public void add ( CalendarEntry calendarEntry ) {
this . session (). saveOrUpdate ( calendarEntry );
}
@Override
public void addAll ( Collection calendarEntries ) {
for ( CalendarEntry each : calendarEntries ) {
this . session (). saveOrUpdate ( each );
}
}
@Override
public void remove ( CalendarEntry calendarEntry ) {
this . session (). delete ( calendarEntry );
}
@Override
public void removeAll ( Collection calendarEntries ) {
for ( CalendarEntry each : calendarEntries ) {
this . session (). delete ( each );
}
}
}
package : infrastructure.persistence
Set 과 유사한 행위 제공
Cascade 를 통한 연관된 엔터티를 같이 변경하는 기능도 있음
복잡한 조회의 경우 HQL을 이용
하지만 JPA를 사용한다면 JPQL을 사용하나, 현재까지는 querydsl 과 같은 기술이 좋은 것 같다.
탑링크 구현에 대한 고려
탑링크는 명시적으로 작업단위(Unit of work) 를 지정할 수 있다.
일종의 트랜잭션의 범위를 추상화 시킨 것
Calendar calendar = session . readObject (...);
UnitOfWork work = session . acquireUnitOfWork ();
Calendar calendarToRename = work . registerObject ( calendar );
calendarToRename . rename ( "CollabOvation Project Calendar" );
work . commit ();
package com . saasovation . collaboration . infrastructure . persistence ;
public class ToplinkCalendarEntryRepository
implements CalendarEntryRepository {
@Override
public void add ( Calendar calendar ) {
this . unitOfWork (). registerNewObject ( calendar );
}
@Override
public void editingCopy ( Calendar calendar ) {
return ( Calendar ) this . unitOfWork (). registerObject ( calendar );
}
}
영속성 지향의 리파지토리
컬렉션 지향 리파지토리 : Set
영속성 지향 리파지토리 : HashMap
하지만 꼭 put 를 해야한다. - 원자적 쓰기를 통제할 수 없음(트랜잭션 없음)
No-Sql 종류가 많다.
잼파이어
코히어런스
몽고DB
리악
코히어런스 구현
package com . saasovation . agilepm . domain . model . product ;
interface ProductRepository {
ProductId nextIdentity ();
Collection allProductsOfTenant ( Tenant tenant );
Product productOfId ( Tenant tenant , ProductId productId );
void remove ( Product product );
void removeAll ( Collection products );
void save ( Product product );
void saveAll ( Collection products );
}
package com . saasovation . agilepm . infrastructure . persistence ;
class CoherenceProductRepository
implements ProductRepository {
private Map caches ;
public CoherenceProductRepository () {
this . caches = new HashMap >();
}
private synchronized NamedCache ( TenantId tenantId ) {
NamedCache cache = this . caches . get ( tenantId );
if ( cache == null ) {
// ageilepm:Product:TenantId
// 1단계 : 2단계 : 3단계
cache = CacheFactory . getCache (
"agilepm.Product." + tenantId . id (),
Product . class . getClassLoader ());
this . caches . put ( tenantId , cache );
}
return cache ;
}
@Override
public void save ( Product product ) {
this . cache ( product . tenantId ())
. put ( this . idOf ( product ), product );
}
@Override
public void saveAll ( Collection products ) {
Map productsMap =
new HashMap >( products . size ());
for ( Product each : products ) {
if ( tenantId == null ) {
tenantId = product . tenantId ();
}
productsMap . put ( this . idOf ( product ), each );
}
this . cache ( tenantId ). putAll ( productsMap );
}
private String idOf ( Product product ) {
return this . idOf ( product . productId ());
}
private String idOf ( ProductId productId ) {
return productId . id ();
}
@Override
public void remove ( Product product ) {
this . cache ( product . tenantId ()). remove ( this . idOf ( product ));
}
@Override
public void removeAll ( Collection products ) {
for ( Product each : products ) {
this . remove ( product );
}
}
@Override
public Collection allProductsOfTenant ( Tenant tenant ) {
Set > entries =
this . cache ( tenant ). entrySet ();
Collection products =
new HashSet ( entries . size ());
for ( Map . Entry entry : entries ) {
products . add ( entry . getValue ());
}
return products ;
}
@Override
public Product productOfId ( Tenant tenant , ProductId productId ) {
return ( Product ) this . cache ( tenant ). get ( this . idOf ( productId ));
}
}
몽고DB 구현
애그리게잇을 -> 몽고DB포맷(직렬화), 몽고DB포맷 -> 애그리게잇(역직렬화)
몽고DB 문서의 고유 식별자(_id )
몽고DB 노드/클러스터 참조
class MongoProductRepository
extends MongoRepository
implements ProductRepository {
public MongoProductRepository () {
super ();
this . serializer ( new BSONSerializer ( Product . class ));
}
public ProductId nextIdentity () {
// 몽고DB에서 제공하는 식별자. _id 필드에 매핑시킬수 있음
return new ProductId ( new ObjectId (). toString ());
}
@Override
public void save ( Product product ) {
this . databaseCollection (
this . collectionName ( product . tenantId ()))
. save ( this . serialize ( product ));
}
protected String collectionName ( TenantId tenantId ) {
return "product" + tenantId . id ();
}
protected String databaseName () {
return "agilepm" ;
}
@Override
public Collection allProductsOfTenant (
TenantId tenantId ) {
Collection products = new ArrayList >();
DBCursor cursor =
this . databaseCollection (
this . databaseName (),
this . collectionName ( tenantId )). find ();
while ( curosr . hasNext ()) {
DBObject dbObject = cursor . next ();
Product product = this . deserialize ( dbObject );
products . add ( product );
}
return products ;
}
@Override
public Product productOfId (
TenantId tenantId , ProductId productId ) {
Product product = null ;
BasicDBObject query = new BasicDBObject ();
query . put ( "producetId" ,
new BasicDBObject ( "id" , productId . id ()));
DBCursor cursor =
this . databaseCollection (
this . databaseName (),
this . collectionName ( tenantId )). find ( query );
if ( cursor . hasNext ()) {
product = this . deserialize ( cursor . next ());
}
return product ;
}
}
class BSONSerializer {
DBObject serialize ( String key , T object ) {
DBObject serialization = this . serialize ( object );
serialization . put ( "_id" , new ObjectId ( key ));
return serialization ;
}
}
abstract class MongoRepository {
protected DBCollection databaseCollection (
String databaseName , String collectionName ) {
return MongoDatabaseProvider
. database ( databaseName )
. getCollection ( collectionName );
}
}
BSONSerializer 때문에 Setter 를 제공할 필요가 없음
추가적인 행동
예를 들면 count() or size()
또는 리파지토리에서 애그리게잇 파트를 쿼리하는 것 (성능상 잇점)
하지만 이건 안티패턴이기 때문에 가능한 사용하지 않는 것이 좋다 (애그리게잇 법칙 위배)
만약 그게 당연시 하다고 느껴지면 애그리게잇을 분리하는 것도 한 방법
일반적으로 도메인 서비스 제어하가 어울린다.
하지만 너무 유스케이스에 최적화된 조회 메서드를 많이 제공한다면 악취일 수도 있다
그런 경우 CQRS 를 고려해보자
트랜잭션의 관리
트랜잭션 관리는 애플리케이션 계층의 책임이다. = 애플리케이션 서비스
트랜잭션 관리 방법
class ApplicationServiceFacade {
// 명식적인 트랜잭션
public void doSomeUseCaseTask1 () {
Transaction transaction = null ;
try {
transaction = this . session (). beginTransaction ();
// 도메인 모델을 사용한다.
transaction . commit ();
} catch ( Exception e ) {
if ( transaction ! null ) {
transaction . rollback ();
}
}
}
// 선언적인 트랜잭션
@Transactional
public void doSomeUseCaseTask2 () {
// 도메인 모델을 사용한다.
}
}
선언적인 방식이 더 낫다. 순수하게 도메인 로직에 위임하는 것에 집중할 수 있다.
관심사 분리!!
경고
단일 트랜잭션에서 여러 애그리게잇을 수정을 커밋하는 기능을 과도하게 사용하지 않도록 주의하라.
타입 계층구조
// 도메인 모델의 클라이언트
serviceProviderRepository . providerOf ( id )
. scheduleService ( date , description );
상위 타입(ServiceProvider )으로 처리
// 도메인 모델의 클라이언트
if ( id . identifiesWarble ()) {
serviceProviderRepository . warbleOf ( id )
. scheduleWarbleService ( date , warbleDescription );
} else if ( id . identifiesWonkle ()) {
serviceProviderRepository . wonkleOf ( id )
. scheduleWonkleService ( date , wonkleDescription );
}
하위 타입을 클라이언트에서 알고 분기 처리 해야하는 책임이 늘어난다.
위 코드는 전형적인 악취이다.
그러므로 상위타입은 하위타입에 대한 구분정보(type 속성)를 알고 있어야 한다.
그렇게 된다면 Repository 계층에서 캡슐화 시켜서 하위 타입 별 분기 처리를 할 수 있을 것이다.
@Entity
class ServiceProvider {
...
private ServiceType type ;
public void scheduleService ( Date date , ServiceDescription description ) {
if ( type . isWarble ()) {
this . scheduleWarbleSevice ( date , description );
} else if ( type . isWonkle ()) {
this . scheduleWonkleService ( date , description );
} else {
this . scheduleCommonService ( date , description );
}
}
}
나라면 위의 경우에서도 ServiceType 으로 위임해서 if 문을 제거했을 것 같다.
리파지토리 대 데이터 액세스 객체
Repository != DAO
리파지토리 : 객체 지향 > with 도메인 모델 패턴
DAO : 데이터 지향 > with 트랜잭션 스크립트 패턴
SMART DAO는 DDD 입장에서는 안티패턴
SMART DAO는 도메인 로직이 DB 쿼리나 DB의 프로시저에 존재하는 경우를 말함
중요한 것은 데이터 액세스 지향보다는 컬렉션 지향으로 설계하려고 노력해야하는 점
리파지토리의 테스트
실제 인프라와 연동하는 테스트 (실제 DB와 통신)
인메모리로 연동하는 테스트
개인적으로 실제 인프라와 연동하는 것이 중요하다고 봄.
인메모리 구현으로 테스트하기
Skip
마무리
컬랙션 지향 VS 영속성 지향
리파지토리의 추가적인 행동 (count() )
트랜잭션 처리
타입 계층과 리파지토리
Repository vs DAO
테스트
IDDD 12장. 리파지토리 was originally published by MJ at DevOOOOOOOOP on June 10, 2018.
source : http://redutan.github.io/2018/06/10/IDDD-chapter12
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
애그리게잇 생성을 캡슐화
도메인 모댈 내의 팩토리
복잡한 객체와 애그리게잇 인스턴스를 생성하는 책임을 별도의 객체로 이동시키자. 여기서의 책임은 도메인 모델과 관련이 있지 않지만, 여전히 도메인 설계를 구성하는 한 요소 다. 모든 복잡한 조립 과정을 캡슐화하고, 클라이언트가 인스턴스화된 객체의 구체적 클래스를 참조할 필요가 없도록 인터페이스를 제공하자. 전체 애그리게잇을 하나의 조각(원자성)으로 생성하고 고정자(Invariant)를 지정하자 [Evans]
팩토리 메소드 : 이 책은 주로 여기만 나옴
팩토리 객체(클래스)
애그리게잇 생성이 매우 복잡하고 다른 객체의 도움이 필요하면 클래스 분리를 하는 것이 좋은 것 같다.
가능한 팩토리 위치
애그리게잇 루트
정정 생성자를 통해서 도메인 의도가 나오면 더 좋다. static Task#forDraft()
아니면 상위 루트에서 하위 엔터리 생성도 가능 Project#createTask()
도메인 서비스 : 서비스 기반 팩토리
ProjectService#createProject()
팩토리 : 이 책은 다루지 않음
ProjectFactory#create()
애그리게잇 루트상의 팩토리 메소드
바운디드 컨텍스트
애그리게잇
팩토리 메소드
식별자와 액세스 컨텍스트
Tenant
offerRegisterationInvitation()
provisionGroup()
provisionRole()
registerUser()
협업 컨텍스트
Calendar
scheduleCalendarEntry()
Forun
startDiscussion()
Discussion
post()
애자일 PM 컨텍스트
Product
planBacklogItem()
scheduleRelease()
scheduleSprint()
CalendarEntry 인스턴스 생성하기
@Entity
class Calendar extends AbstractAggregateRoot {
CalendarEntry scheduleCalendarEntry (...) {
CalendarEntry calendarEntry = new CalendarEntry (...);
this . registerEvent ( new CalendarEntryScheduled (...));
return calendarEntry ;
}
}
유비쿼터스 언어에 부합되는 도메인이 표현됨 : scheduleCalendarEntry
보호절이 없다 : 어짜피 new CalendarEntry(...) 가 책임짐
Setter 를 사용하지 않는다. > 원자성 + 부수효과 줄어듬 + 불변식 강제 가 쉬움
이벤트 발행 : CalendarEntryScheduled
인자의 갯수가 줄어든다.
scheduleCalendarEntry(...) 에서는 11개가 요구되지만, new CalendarEntryScheduled(...) 에서는 9개로 줄어든다.
개인적으로는 이것도 인자를 캡슐화 시켜서 객체로 말아버리는 것이 좋은 것 같다. 9개의 인자라도 너무 많은 것 같음.
하지만
CalendarEntry 를 생성하기 위해서는 꼭 Calendar 인스턴스 가 요구됨
이는 DB 조회라는 부하가 추가됨
Discussion 인스턴스 생성하기
@Entity
class Forum extends AbstractAggregateRoot {
Discussion startDiscussion (
DiscussionId discussionId ,
Author author ,
String subject ) {
if ( this . isClosed ()) {
throw new IllegalStateException ( "Forum is closed" );
}
Discussion discussion = new Discussion (
this . tenant (),
this . forumId (),
discussionId ,
author ,
subject );
registerEvent ( new DiscussionStarted (...));
return discussion ;
}
}
포럼이 열린 경우에만 토론을 시작할 수 있다. : this.isClosed()
인자 5개 중 3개만 있으면 된다. : 나머지 2개는 포럼에서 제공
역시나 유비쿼터스 언어가 표현된다. : startDiscussion
서비스의 팩토리
package com . saasovation . collaboration . domain . model . collaborator ;
interface CollaboratorSerice {
Author authorFrom ( Tenant tenant , String identity );
Creator creatorFrom ( Tenant tenant , String identity );
Moderator moderatorFrom ( Tenant tenant , String identity );
Owner ownerFrom ( Tenant tenant , String identity );
Participant participantFrom ( Tenant tenant , String identity );
}
// infrastructure.services !!!
package com . saasovation . collaboration . infrastructure . services ;
class UserRoleToCollaborationService implements CollaboratorSerice {
@Override
public Author authorFrom ( Tenant tenant , String identity ) {
return ( Author ) userInRoleAdapter . toCollaborator (
tenant , identity , "Author" , Author . class );
)
}
}
package com . saasovation . collaboration . domain . model . collaborator ;
class Author extends Collaborator {
...
}
기술적 구현이므로 인프라 계층 의 모듈에 위치한다.
어댑터에 의존한다.
UserInRoleAdapter 는 외래 컨텍스트와 의사소통 책임만 갖는다.
CollaboratorTranslator 는 바운디드 컨텍스트 내 도메인 객체로 변환 책임만 갖는다.
협업에서는 identity 식별자와 액세스에서는 username
마무리
유비쿼터스 언어로 애그리게잇을 생성
애그리게잇의 팩토리 메서드 VS 서비스의 팩토리 메서드
IDDD 111장. 팩토리 was originally published by MJ at DevOOOOOOOOP on June 07, 2018.
source : http://redutan.github.io/2018/06/07/IDDD-chapter11
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
일관성 경계 의 기준은 같은 트랜잭션인가로 검증된다.
애그리게잇 내의 불변식(invariant)?
스크럼 핵심 도메인에서 애그리게잇 사용하기
기능 목록
제품은 백로그 아이템과 릴리스, 스프린트를 포함한다.
새로운 제품 백로그 아이템을 계획했다.
새로운 제품 릴리스를 계획했다.
새로운 제품 스프린트 일정을 수립했다.
계획된 백로그 아이템에 관한 릴리스 일정을 수립할 수 있다.
일정이 잡힌 백로그 아이템은 스프린트로 커밋할 수 있다.
첫 번째 시도: 큰 클러스터의 애그리게잇
제품이 ~를 포함한다.
컴포지션 VS 객체 그래프
포함 VS (상호) 연결
도메인 로직
백로그 항목을 스프린트로 커밋하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
스프린트가 백로그 항목을 커밋하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
릴리스가 백로그 항목의 일정을 수립하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
백로그 항목의 릴리스 일정을 수립하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
class Product {
private Set backlogItems ;
private String description ;
private String name ;
private ProductId productId ;
private Set releases ;
private Set sprints ;
private TenantId tenantId ;
}
아주 큰 애그리게잇으로 모델링된 Product
Product와는 별도의 애그리게잇 타입으로 모델린된 연관된 개념들
큰 애그리게잇으로 모델링하다보면 변경에 취약해져서 업데이트 상황에서 버전 충돌이 발생할 가능성이 커진다. 위를 예시로 두면 한 명이 backlogItem을 변경하고 다른 한 명이 Spring를 변경할 시 직적접 연관이 없음에도 불구하고 버전 충돌이 발생해서 업데이트가 실패할 확률이 커진다. (애그리게잇의 크기가 커짐에 따라서 충돌 확률도 더 커짐)
두 번째 시도: 다수의 애그리게잇
하나의 큰 애그리게잇 상 Product.java
class Product {
public void planBacklogItem (
String summary , String category , BacklogItemType type , StoryPorints storyPoints ) {
...
}
...
public void scheduleRelease (
String name , String description , Date begins , Date ends ) {
...
}
...
public void scheduleSprint (
String name , String goals , Date begins , Date ends ) {
...
}
...
}
여러 개로 분리된 애그리게잇 상 Product.java
class Product {
public BacklogItem planBacklogItem (
String summary , String category , BacklogItemType type , StoryPorints storyPoints ) {
...
}
...
public Release scheduleRelease (
String name , String description , Date begins , Date ends ) {
...
}
...
public Sprint scheduleSprint (
String name , String goals , Date begins , Date ends ) {
...
}
...
}
일종의 factory 메서드로써 동작한다.
Product *Srvice 예시
@Service
class ProductBacklogItemService {
@Transactional
public void planProductBacklogItem (
String tenantId , String productId
String summary , String category
String backlogItemType , String storyPoints ) {
Product product =
productRepository . producetOfId (
new TenantId ( tenantId ),
new ProductId ( productId ));
BacklogItem plannedBacklogItem =
product . planBacklogItem (
summary , category ,
BacklogItemType . valueOf ( aBacklogItemType ),
StoryPoints . valueOf ( stroyPoints ));
backlogItemRepository . add ( plannedBacklogItem );
}
...
}
이와 같이 우린 밖으로 빼서 모델링함으로써(Modeling it away) 트랜잭션 실패 문제를 해결했다. 이제 BacklogItem , Release , Sprint 등의 인스턴스가 사용자의 요청에 따라 얼마든지 동시적으로 안전하게 생성될 수 있다.
그러나 큰 애그리게잇을 조금 다듬어서 동시성 문제를 해결할 수도 있을지도 모른다. 하이버네이트 매핑에서 optmistic-lock 옵셥을 false 로 설정해 트랜잭션 실패가 도미노 처럼 전달되는 상황을 피할 수 있다.
규칙: 진짜 고장자(invariant)를 일관성 경계 안에 모델링하라
*중요한 것은 진짜 고정자를 이해하는 것이다.
고장자(invariant) : 일관성(트랜잭션 일관성)을 유지해야만 한다는 비즈니스 규칙
트랜잭션 일관성 : 동기적, 원자적
결과적 일관성 : 비동기적
한 트랜잭션에 한 애그리게잇만 인스턴스만 포함 : 이는 너무 가혹한 것 같다.
규칙: 작은 애그리게잇으로 설계하라
이 Product 모델에선 다양한 기본 오퍼레이션이 수행될 동안 큰 컬랙션을 여럿 가져오게 된다.
이 큰 클러스터 애그리게잇은 성능이나 확장성이 절대로 좋을 수 없다. 이는 실패로 이어지는 악몽이 될 뿐이다. 거짓 고정자와 컴포지션적 편의성이 설계를 주도했기 때문에 시작부터 문제가 있었으며, 트랜잭션의 종료, 성능, 확장성의 측면에서 안 좋은 영향을 미쳤다.
작은 애그리게잇은? 다른 대상과 일관성을 유지
변경이 되면 엔터티
대치가 되면 값 객체 : 생각보다 상당히 많은 개념이 값 객체로 대치된다.
파생 금융상품 부문에서 약 70% 애그리게잇이 단 하나의 루트 엔터티로 구성된다.
작은 애그리게잇은
성능이 좋음
확장성이 좋음
트랜잭션이 성공할 가능성이 크다
유스케이스를 전부 믿지는 말라
하나의 유스케이스가 여러 트랜잭션을 발생 시킨다면 의심해 보자
이런 경우에서 결과적 일관성을 통해서 문제를 해결할 수 있다. + 지연 업데이트
물론 하나의 유스케이스가 하나의 트랜잭션일 필요는 없다.
규칙: ID로 다른 애그리게잇을 참조하라
객체 그래프가 연결되어 있다고 해서 같은 애그리게잇은 아니다. 그저 다른 애그리게잇을 연결했을 뿐이다.
여기도 결국 결과적 일관성 으로 이어진다.
애그리게잇 ID 참조를 통해 서로 함께 동작하도록 해보자
ID를 통해 경계 밖과 연결을 추론할 수 있는 BacklogItem 애그리게잇
class BacklogItem {
private ProductId productId ;
}
모델 탐색
객체 그래프 탐색과는 다르지만 리파지토르 와 ID 가 있으면 연관 모델을 탐색할 수 있다. : 단전될 도메인 모델(Disconnected Domain Model)
@Service
class ProductBacklogItemService {
@Transactional
public void assignTeaMemberToTask (
String aTenantId ,
String aBacklogItemId ,
String aTaskId ,
String aTeamMemberId ) {
BacklogItem backlogItem =
backlogItemRepository . findById (
new TenantId ( aTenantId ), new BacklogItemId ( aBacklogItemId ));
Team ofTeam =
teamRepository . findById (
backlogItem . tenantId (), backlogItem . teamId ());
backlogItem . assignTeamMemberToTask (
new TeamMemberId ( aTeamMemberId ), ofTeam , new TaskId ( aTaskId ));
}
}
확장성과 분산
ID 참조를 이용하게 되면 같은 영속화 플랫폼을 사용하지 않고 샤딩과 같은 확장을 통해서 일부 애그리게잇을 손쉽게 확장할 수 있다.
예를 들면 어떤 애그리게잇은 DB를 사용하고 연관되는 다른 애그리게잇은 NoSql을 사용할 수 있다.
도메인 이벤트를 통해서 외부 바운디드 컨텍스트로 분산처리를 더 가속화할 수 있다.
역시나 중요한 것은 결과적 일관성 이다.
규칙: 경계의 밖에서 결과적 일관성을 사용하라
결과적 일관성과 지연 시간 = 도메인 이벤트 발행
동시성 이슈로 인해서 발행된 이벤트 구독이 실패하면? 메시징 매커니즘을 통해서 Retry! > 이것은 쉽지가 않은 것 같다.
누가 해야 하는 일인지 확인하자
데이터의 일관성을 보장하는 주체가 유스케이스를 수행하는 사용자의 일인지를 질문해보자.
만약 그렇다면, 다른 애그리게잇의 규칙들은 고수하는 가운데 트랜잭션을 통해 일관성을 보장하도록 하자.
만약, 다른 사용자나 시스템이 해야 할 일이라면 결과적 일관성을 선택하자.
규칙을 어겨야하는 이유
첫 번째 이유: 사용자 인터페이스의 편의
두 번째 이유: 기술적 매커니즘의 부족
세 번째 이유: 글로벌 트랜잭션
개인적으로 안티패턴. 결과적 일관성을 사용하자
네 번째 이유: 쿼리 성능
캐싱을 통해서 어느정도 해결할 수 있다.
발견을 통해 통찰 얻기
Skip
구현
고유 ID와 루트 엔터리를 생성하라
값 객체 파트를 선호하라
‘데메테르 법칙’과 ‘묻지 말고 시켜라’를 사용하기
데메테르 법칙 : 정보은닉
묻지 말고 시켜라 : 정보은닉 + 응집력
낙관적 동시성
애그리게잇 루트에 버전을 통한 낙관적 락 기법
@Version : JPA를 이용하면 이 선언만으로도 낙관적 락 기법을 사용할 수 있다.
@Entity
@Table ( name = "orders" )
public class Order {
@Id
private Long id ;
@Version
private int version ;
private String description ;
}
의존성 주입을 피하라
애그리게잇에 서비스나 리파지토리를 주입하지 마라
마무리
가능하면 작은 애그리게잇으로 설계하자
일관성 경계, 트랜잭션, 고정자가 중요
객체 그래프 참조 VS ID 참조
경계 외부에서는 결과적 일관성
IDDD 10장. 애그리게잇 was originally published by MJ at DevOOOOOOOOP on June 05, 2018.
source : http://redutan.github.io/2018/06/05/IDDD-chapter10
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
팀 내에서 IDDD(Implementing Domain-Driven Design) 스터디 중 값 객체 Collection을 ORM(hibernate)을 이용하여 구현하는 예제 를 확인했습니다.
그러던 중 이게 아주 과거 hibernate xml 구성 기준이어서 현재 JPA(Sprin Data JPA)에서는 어떻게 구현되는지 궁금 해 하던 @동묘 가 저에게 직접 구현을 보고 싶다고 해서 포스팅 하게 되었습니다.
구현
3가지 구현 방법
Single Column
Entity
Join Table
예시 도메인
그룹은 애그리게잇 루트(엔터티)입니다.
그룹맴버는 값 객체입니다.
한 그룹에는 여러 그룹맴버가 있습니다. (Group#groupMembers )
그룹과 그룹맴버는 한 애그리게잇으로 묶입니다.
Many Values Serialized into a Single Column
groups.group_members 에 그룹맴버들 객체를 직렬화해서 저장합니다. 여기에서는 varchar(4000) 데이터타입으로써 JSON으로 직렬화 하겠습니다.
Java
GroupMember.java
@Embeddable
@Value
public class GroupMember {
private String name ;
@Enumerated ( EnumType . STRING )
private GroupMemberType type ;
// For JPA
GroupMember () {
this . name = null ;
this . type = null ;
}
// For Jackson
@JsonCreator
public GroupMember ( @JsonProperty ( "name" ) String name ,
@JsonProperty ( "type" ) GroupMemberType type ) {
this . name = name ;
this . type = type ;
}
}
Group.java
@Entity
@Table ( name = "GROUPS" )
@Getter
@EqualsAndHashCode
@ToString
@NoArgsConstructor ( access = AccessLevel . PACKAGE )
public class Group {
@Id
@GeneratedValue
private Long groupId ;
private String description ;
private String name ;
/**
* ORM과 한 열로 직렬화되는 여러 값 : ORM and Many Values Serialized into a Single Column
*/
@Convert ( converter = GroupMembersConverter . class )
@Column ( name = "GROUP_MEMBERS" , length = 4000 )
private Set group1Members = new HashSet >();
...
}
GroupMembersConverter.java
@Converter
public class GroupMembersConverter implements AttributeConverter , String > {
private ObjectMapper om = new ObjectMapper ();
@Override
public String convertToDatabaseColumn ( Set attribute ) {
return om . writeValueAsString ( attribute );
}
@Override
public Set convertToEntityAttribute ( String dbData ) {
return om . readValue ( dbData , new TypeReference >() { });
}
}
SQL
Schema
create table groups (
group_id bigint not null ,
description varchar ( 255 ),
group_members varchar ( 4000 ), /* !!! */
name varchar ( 255 ),
primary key ( group_id )
);
Insert
insert into groups (
description , group_members , name , group_id
) values (
? , ? , ? , ?
);
Many Values Backed by a Database Entity
실질적으로는 Entity처럼 DB 스키마를 구성 하나 실제 객체지향세계(ex:Java Application)에서는 값 객체로 보이게 구현
이를 위해서 상속을 이용해서 식별자 속성을 은닉 시키는 것이 구현의 핵심
Java
IdentifiedValueObject.java
@MappedSuperclass
@NoArgsConstructor ( access = AccessLevel . PROTECTED )
public abstract class IdentifiedValueObject {
@Id
@GeneratedValue
@Getter ( AccessLevel . PACKAGE )
@Setter ( AccessLevel . PACKAGE )
private Long id ; // 패키지 접근제어를 통해서 식별자 은닉. 단, hibernate는 접근 가능
}
GroupMember.java
@Entity
@Table ( name = "GROUP_MEMBERS" )
@Value
@EqualsAndHashCode ( callSuper = false )
public class GroupMember extends IdentifiedValueObject {
private String name ;
@Enumerated ( EnumType . STRING )
private GroupMemberType type ;
// For JPA
GroupMember () {
this . name = null ;
this . type = null ;
}
}
Group.java
public class Group {
...
/**
* ORM과 데이터베이스 엔터티로 지원되는 여러 값 : ORM and Many Values Backed by a Database Entity
*/
@OneToMany ( cascade = CascadeType . ALL , orphanRemoval = true ) // Aggregate Root 를 위한 일관성 설정
@JoinColumn ( name = "GROUP_ID" )
private Set groupMembers = new HashSet >();
...
}
SQL
Schema
create table groups (
group_id bigint not null ,
description varchar ( 255 ),
name varchar ( 255 ),
primary key ( group_id )
);
create table group_members (
id bigint not null , // PK !!
name varchar ( 255 ),
type varchar ( 255 ),
group_id bigint ,
primary key ( id )
)
alter table group_members
add constraint fk_groups_group_members
foreign key ( group_id ) references groups ;
Insert
insert into groups ( description , name , group_id ) values ( ? , ? , ? )
insert into group_members ( name , type , id ) values ( ? , ? , ? )
insert into group_members ( name , type , id ) values ( ? , ? , ? )
update group_members set group_id =? where id =? /* Update ?! */
update group_members set group_id =? where id =?
일반적으로 가장 무난하며, IDDD 책에서는 저자가 가장 추천한 방식
Many Values Backed by a Join Table
group_members 을 테이블로 분리하는데 이 테이블은 PK가 없이 groups 의 PK를 FK로 가지는 Join Table로 구현
Java
Group.java
public class Group {
@Id
@GeneratedValue
private Long groupId ;
private String description ;
private String name ;
/**
* ORM과 조인 테이블로 지원되는 여러 값 : ORM and Many Values Backed by a Join Table
*/
@ElementCollection
@CollectionTable ( name = "GROUP_MEMBERS" , joinColumns = @JoinColumn ( name = "GROUP_ID" ))
private Set groupMembers = new HashSet >();
...
}
SQL
Schema
create table groups (
group_id bigint not null ,
description varchar ( 255 ),
name varchar ( 255 ),
primary key ( group_id )
);
create table group_members ( /* No PK */
group_id bigint not null ,
name varchar ( 255 ),
type varchar ( 255 )
);
alter table group_members
add constraint fk_groups_group_members
foreign key ( group_id ) references groups ;
Insert
insert into groups ( description , name , group_id ) values ( ? , ? , ? );
insert into group_members ( group_id , name , type ) values ( ? , ? , ? );
insert into group_members ( group_id , name , type ) values ( ? , ? , ? );
이슈
값 객체 Collection에 새로운 값이 추가되거나 기존 값이 변경될 시 All Delete and Re-insert
@ElementCollection 을 통해 변경이 발생할 시, 전체를 지우고 다시 입력하는 이슈가 있음.
이를 완화시키기 위해서 @OrderColumn 을 추가하는 방법이 있긴하나 완벽하지는 않음
public class Group {
...
@ElementCollection
@CollectionTable ( name = "GROUP_MEMBERS" , joinColumns = @JoinColumn ( name = "GROUP_ID" ))
@OrderColumn // !!!
private Set groupMembers = new HashSet >();
...
}
Summary
Single Column
Entity
Join Table
Reference
소스코드 : https://github.com/redutan/ddd-values
Implementing Domain-Driven Design. Vaughn Vernon
자바 ORM 표준 JPA 프로그래밍. 김영한
http://kwonnam.pe.kr/wiki/java/jpa/elementcollection
JPA에서 Value Object Collection 3가지 구현 was originally published by MJ at DevOOOOOOOOP on May 29, 2018.
source : http://redutan.github.io/2018/05/29/ddd-values-on-jpa
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
C#: 네임스페이스
Roby: 모듈
모듈로 설계하기
모듈 : 도메인 객체의 컨테이너
규칙
모델링의 개념에 맞춰 모듈을 설계하자
유비쿼터스 언어에 맞춰 모듈을 명명하자
모델에서 사용하는 일반적인 컴포넌트 타입이나 패턴에 따라서 기계적으로 모듈을 생성하지 말자
느슨하게 결합된 모듈을 설계하자.
결합이 필요하다면 짝이 되는 모듈 사이에서 비순환적 의존성이 형성되도록 노력하자
자식 모듈과 부모 모듈 사이에 규칙은 느슨하게 하자
모듈을 모델의 정적인 개념에 따라 만들지 말고, 모듈이 담고 있는 객체에 맞추도록 하자
주방의 서랍에 식기류가 포크와 나이프와 스푼별로 잘 정리돼 있음
기본 모듈 명명 규칙
Java: com.saasovation
C#: SaaSOvation
모델을 위한 모듈 명명 규칙
바운디드 컨텍스트
com.saasovation.identityaccess
com.saasovation.collovoration
com.saasovation.agilepm
모듈 구성 예시
com.saasovation.identityaccess
domain.model
결론은 여러분 팀의 몫이다.
애자일 프로젝트 관리 컨텍스트의 모듈
com.saasovation.agilepm
domain
model
product
backlogitem
release
sprint
tenant
team
이 팀은 모듈 사이의 결합도 문제보다 정리 에 중점을 뒀다.
다른 계층 속의 모듈
계층
모듈
비고
사용자 인터페이스
com.saasovation.agilepm.resources
ui
애플리케이션
com.saasovation.agilepm.application
도메인
com.saasovation.agilepm.domain
인프라
com.saasovation.agilepm.infrastructure
adapter
바운디드 컨텍스트보다 모듈
모듈은 응집력이 낮은 경우 분리하기 위한 컨테이너이고, 바운디드 컨텍스트는 그저 경계일 뿐이다.
어짜피 모듈은 상향식(bottom-up)으로 설계하게된다.
모듈 내의 컴포넌트들에 따라서 나눠질 수도 합쳐질 수도 있다. 중요한 것은 도메인 로직과 유비쿼터스 언어에 따르는 것이다.
마무리
유비쿼터스 언어를 표현한다.
모듈 설계의 예시!!
기계적인 모듈 설계는 창의성을 방해한다.
바운디드 컨텍스트 분리보다 모듈 사용이 먼저다.
IDDD 9장. 모듈 was originally published by MJ at DevOOOOOOOOP on May 28, 2018.
source : http://redutan.github.io/2018/05/28/IDDD-chapter09
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
언제 그리고 왜 도메인 이벤트를 사용할까?
도메인 내 어떤 사건이 발생했을 때
한 트랜잭션에는 한 애그리게잇만 커밋
이벤트의 모델링
어떤 명령에서 어떤 사건이 발생했었음
Command : BacklogItem#commitTo(Spring)
Event : BacklogItemCommitted
백로그 항목이 커밋됐다.(과거형)
package com . saasovation . agilepm . domain . model . product ;
@Value
class BacklogItemCommitted implements DomainEvent {
Date occurredOn ;
BacklogItemId backlogItemId ;
SpringId committedToSprintId ;
TenantId tenantId ;
}
package com . saasovation . agilepm . domain . model ;
interface DomainEvent {
Date occurredOn ();
}
추가사항
멱등성
더 풍부한 상태 전달 (이벤트 소싱!)
애그리게잇의 특성과 함께하기
이벤트를 애그리게잇을 통해서 영속화
식별자
이벤트를 애그리게잇으로 모델링하면 식별자가 필요하다.
도메인 이벤트를 바운디드 컨텍스트 외부로 발행 시 식별자가 필요(With rabbitmq)
외부 구독자 입장에서는 멱등성 관리를 위해서 식별자를 할당할 수 있음
equals 로 일정 부분 해결할 수도 있다 (in 로컬 바운디드 컨텍스트)
도메인 모델에서 이벤트 발행하기
Light-Weight Publish-Subscribe
Reference : https://github.com/redutan/redutan.github.io/wiki/Domain-Event-on-Springframework
발행자
class BacklogItem extends AbstractAggregateRoot {
void commitTo ( Sprint spint ) {
// Some domain logic
super . registerEvent ( new BacklogItemCommitted (
this . tenantId (),
this . backlogItemId (),
this . sprintId ()
));
}
}
class BacklogItemService { // Application Service
@TransactionalEventListener
void commitBacklogItem (...) {
backlogItem . commitTo ( sprint ); // Publish BacklogItemCommitted event
}
}
구독자
class BacklogItemService { // Application Service
@TransactionalEventListener
void handleBacklogItemCommitted ( BacklogItemCommitted event ) {
// BacklogItemCommitted 구독 후 처리
}
}
중요한 것은 결과적 일관성
뉴스를 원격 바운디드 컨텍스트로 전파하기
시스템 간 결과적 일관성 확보
메시징 인프라의 일관성
구현방법
도메인 모델과 메시지 인프라 저장소 공유
원격 DB with XA
이벤트 저장소
자치 서비스와 시스템
자치 서비스 : 이벤트를 통해서 시스템 간 결합도(독립성!)를 줄이는 기법 (No-RPC)
자치 서비스의 단위는 바운디드 컨텍스트가 되면 좋은 것 같다.
지연 시간 허용
결과적 일관성 을 위해서
도메인 별로 그 때 그 때 달라요.
시스템의 허용치를 만족시키면서도 잘 수행되도록 아키텍처 품질을 높여야 한다.
이벤트 저장소
이벤트의 상태를 유지하기 위해서 저장하는 것이 요구되는 경우가 많다.
@Aspect
class IdentityAccessEventProcessor {
@Before ( "execution(* com.saasovation.identityaccess.application.*.**..))" )
public void listen () {
DomainEventPublisher . instance ()
. subscribe ( new DomainEventSubscriber () {
public void handleEvent ( DomainEvent aDomainEvent ) {
store ( aDomainEvent );
}
public Class subscribedToEventType () {
return DomainEvent . class ; // 모든 도메인 이벤트
}
});
}
private void store ( DomainEvent aDomainEvent ) {
EventStore . instance (). append ( aDomainEvent );
}
}
class StoreEvent {
void append ( DomainEvent aDomainEvent ) {
String eventSerializatoin =
EventStore . objectSerializer (). serialize ( aDomainEvent );
StoredEvent storedEvent =
new StoredEvent (
aDomainEvent . getClass (). getName (),
aDomainEvent . occuredOn (),
eventSerialization );
this . session (). save ( storedEvent );
this . setStoredEvent ( storedEvent );
}
}
CREATE TABLE tbl_stored_event (
event_id int ( 11 ) NOT NULL auto_increment ,
event_body varchar ( 65000 ) NOT NULL ,
occurred_on datetime NOT NULL ,
type_name varchar ( 100 ) NOT NULL ,
PRIMARY KEY ( event_id )
)
저장된 이벤트의 전달을 위한 아키텍처 스타일
레스트품 리소스로서 알림 발행하기
이벤트를 REST/WEB API로 발행한다. (이벤트 아이디나 애그리게잇 아이디를 전달)
이벤트 저장소를 통해서 조회하거나, 애그리게잇을 직접 조회한다.
구독 측에서 발행측에 다시 상세 이벤트 정보를 조회한 후 처리한다.
메시징 미들웨어를 통한 알림 발행
메시징 미들웨어를 통해서 이벤트를 발행한다.
메시징 미들웨어를 구독한 구독 측에서 발행 측의 상세 이벤트 정보를 조회한 후 처리 한다.
발행과 구독은 exchange나 queue 개념으로 연결된다.
구현
Skip
멱등 수신자 처리가 중요하다 : At least once
멱등성 : 오퍼레이션이 두 번 이상 수행되어도, 한 번만 수행했을 때와 같은 결과에 이르는 동작을 의미
궁극적으로 이벤트를 추적해야하는 경우가 생기기 때문에 이벤트 저장 기능이 거의 필수적으로 요구된다.
그리고 추적 일관성을 위해서 이벤트 저장은 도메인 로직과 트랜잭션으로 묶이는 것이 중요하다.
마무리
이벤트 만의 고유 식별자가 요구된다.
이벤트 저장소도 필요
어짜피 발행-구독!
멱등성 또는 중복 발행 or 수신 제거 확보
IDDD 8장. 도메인 이벤트 was originally published by MJ at DevOOOOOOOOP on May 24, 2018.
source : http://redutan.github.io/2018/05/24/IDDD-chapter08
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
도메인이 기술 보다 먼저다.
성공한 CIO와의 인터뷰
Server-Client Architecture
계층형 아키텍처
DIP, QP, PSA, DDD-Lite
헥사고날 아키텍처
To Mobile, Cloud
CQRS
Materialized Views
Event-Driven Architecture
Pipeline + Filter
Saga 패턴
Long-lived transaction as a sequence of subtransactions.
In a distributed system
이벤트 소싱
모든 변경을 추적 (ex: 보안 감사 등)
이러한 아키텍처 덕분에 결국 회사에 돈이 된다
계층
그림 4.1 DDD가 적용된 전통적인 계층 아키텍처
[출처] https://ajlopez.wordpress.com/2008/09/12/layered-architecture-in-domain-driven-design/
도메인 모델과 비즈니스 로직을 도메인 계층에 격리
하위 계층에만 의존 - 아래에서 위쪽으로 직접 참조 불가 (하지만 Observer 사용 가능)
느슨한 연결 : UI가 애플리케이션(바로 하위) 뿐만 아니라 도메인이나 인프라에도 의존 가능
UI 계층
View나 API 제공
도메인 모델이 아닌 표현 모델(DTO)를 사용 추천
애플리케이션 계층
UI로 부터 매개변수를 받아 리파지토리를 사용해 애그리게잇을 획득하고, 커맨드를 위임
보안과 트랜젝션 담당 (일반적으로 보안을 UI로 올리는 것 같음)
도메인 로직 없으며 도메인 모델(애그리게잇, 도메인 서비스 등)에 위임
도메인 모델에서 발행한 도메인 이벤트를 구독(subscribe)
@Transactionl
void commitBackLogItemToSpring (
String aTanantId , String aBackLogItemId , String aSprintId ) {
// 재료(Aggregate) 준비
BacklogItem backlogItem =
backlogItemRepository . backlogItemOfId ( aTanantId , aBacklogItemId );
Sprint sprint sprintRepository . sprintOfId ( aSprintId );
// 커맨드 위임
backlogItem . commitTo ( sprint );
}
DIP
상위 수준의 모듈은 하위 수준 모듈에 의존해선 안된다. 둘 모두는 반드시 추상화에 의존해야 한다.
추상화는 세부사항에 의존해선 안 된다. 세부사항은 추상화에 의존해야 한다.
하위 구현 컴포넌트가 상위 콤포넌트가 정의한 인터페이스에 의존해야한다.
어라 그러다 보니 계층이 없어진다. 여기에 대칭성을 더하면 어떻게 될까?
헥사고날 또는 포트와 어댑터
헥사고날 아키텍처
[출처] http://alistair.cockburn.us/Hexagonal+architecture
Texi handling Hexagonal Architecture
[출처] https://github.com/seongminwoo/study/blob/master/7-part_series_about_microservices.md
헥사고날의 장점
기능적 요구사항에 따라 애플리케이션 내부를 설계
UI, Infra는 그 다음이다.
애플리케이션 내부가 캡슐화 된다.
Adapter를 통해 기술과 분리
서비스 지향(SOA)
중요한 것은 기술 보다 비즈니스(도메인) 가치가 우선해야 한다
REST: 표현 상태 전송
[출처] https://martinfowler.com/articles/richardsonMaturityModel.html
RESTful HTTP 서버의 주요 특징
Resource : URI
Verbs : GET, POST, PUT, DELETE, …
Hypermedia : 연결된 리소스를 제공. 상호작용, 클라이언트 무상태
RESTful HTTP 클라이언트의 주요 특징
Hypermedia를 바탕으로 상호 작용한다.
REST와 DDD
도메인 모델을 RESTful로 바로 노출하는 것은 좋지 않다.
도메인 모델의 변경이 API와 연결되기 때문에 변경에 취약해지며, 클라이언트와 호환성이 깨진다.
해결책
도메인 모델 -> 표현 모델을 조립 (추천)
Assembler + DTO (PoEAA)
각 상황 별 공유 도메인 모델 사용
공유커널
왜 REST인가
느슨함
CQRS
사용자가 필요로하는 데이터 뷰를 리파지토리로 쿼리하기란 어려울 수 있다.
CQRS = 객체 설계 원칙 + CQS
Command : 객체의 상태를 수정, Void 형
Query : 값을 반환, 수정 X
구현
애그리게잇은 오직 커맨드 메소드만 가지고 있음.(커맨드 모델)
쿼리를 위한 뷰 전용 모델(쿼리 모델)을 생성
CQRS의 영역 살펴보기
출처 : https://docs.microsoft.com/ko-kr/azure/architecture/guide/architecture-styles/cqrs
참고 : https://docs.microsoft.com/ko-kr/azure/architecture/patterns/cqrs
클라이언트와 쿼리 처리기
쿼리 모델(읽기 모델)
필요한 수 만큼 뷰를 지원하기
실용적으로 하라
데이터베이스 테이블 뷰가 오버헤드의 원인이 되지 않을까?
클라이언트가 커맨트 처리를 주도한다.
UI to Argument
커맨드 처리기
카테고리 스타일 : 1 class n methods
전용 스타일 : 1 class 1 method
커맨드 모델(쓰기 모델)은 행동을 수행한다.
public void commitTo ( Sprint aSprint ) {
...
DomainEventPublisher
. instance ()
. publish ( new BacklogItemCommitted (
this . tenant (),
this . backlogItemId (),
this . sprintId ()
));
}
어떤 경우든 쿼리 모델을 업데이트시키기 위해선 도메인 이벤트를 게시해야 한다.
이벤트 구독자가 쿼리 모델을 업데이트 한다.
동기도 가능하고 비동기 도 가능하다.
결국은 일관성이 유지되는 쿼리 모델 다루기
Eventually consistent
지연되는 뷰 데이터 동기화를 해결해야한다.
낙관적 업데이트 기법!
클라이언트에서 publish/subscribe 기법
결국은 동기화되는 지연시간이 문제
이벤트 주도 아키텍처(EDA)
이벤트의 생산, 감지, 소비와 이벤트에 따른 응답 등을 촉진하는 소프트웨어 아키텍처
From 도메인 이벤트
파이프와 필터
$ cat phone_number.txt | grep 303 | wc -1
Spring Cloud Data Flow
그림 4.8 필터를 처리하는 이벤트를 보냄으로써 파이프라인이 만들어진다.
[출처] http://zhangyi.farbox.com/post/coding/understand-scala-stack
이는 가상의 예제이자 개념적 부분을 강조했을 뿐이다. 실제 엔터프라이즈에선 큰 문제를 좀 더 작은 단계로 나누기 위해 이 패턴을 사용하며, 좀 더 쉽게 분산 처리를 이해하고 관리하도록 해준다.
또한 여러 시스템이 오직 자신이 할 일(도메인)만을 걱정하게 되게 해주기도 한다.
장기 실행 프로세스
컴포지트
애그리게잇 집합
이벤트
역시나 중요한 것은 결과적 일관성 (Eventual Consistency)
Saga Pattern
Events/Choreography
Rollback
referecne : https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/
이벤트 소싱
거의 완벽한 수준의 변경 추적
[출처] https://docs.microsoft.com/ko-kr/azure/architecture/patterns/event-sourcing
Replay : 이벤트소싱에서 이벤트를 되감아서 특정 버전 상태로 되돌리는 것
하지만 최신 버전 Replay는 병목의 원인
이를 해결하기 위해서 최신 버전의 상태를 Snapshot 으로 지정해서 최적화
데이터 패브릭과 그리드 기반 분산 컴퓨팅
분산 캐시
ex) hazelcast
데이터 복제
캐시 master/slave
복제를 통한 장애 극복
이벤트 유실 제거 (이벤트 publish/subscribe도 가능)
이벤트 주도 패브릭과 도메인 이벤트
For Domain Event
지속적 쿼리
For CQRS
분산 처리
For Saga 또는 배치 병렬 처리
마무리
Layerd Architecture > DIP > Hexagonal Architecture
SOA, REST, 분산 컴퓨팅
CQRS, Event Sourcing, pipe/filter, Saga
IDDD 4장. 아키텍처 was originally published by MJ at DevOOOOOOOOP on May 08, 2018.
source : http://redutan.github.io/2018/05/08/IDDD-chapter04
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
컨텍스트 맵이 필수적인 이유
그림 3.1 추상적 도메인의 컨텍스트 맵
[출처] https://www.safaribooksonline.com/library/view/implementing-domain-driven-design/9780133039900/ch03lev1sec1.html
U = 업스트림
D = 다운스트림
컨텍스트 맵 은 상호 교류하는 시스템의 목록을 제공하고, 팀 내 의사소통의 촉매 역할을 한다.
프로젝트와 조직 관계
통합 패턴들
파트너십(Partnership) : 두 컨텐스트가 한 트랜젝션으로 묶임 - 2 phase commit?
공유 커널(Shared kernel) : 상호 의존하는 공유 모델을 관리 - 안티 패턴이라고 봄
고객-공급자(Customer-Supplier Development) : 업스트림(서버:공급자), 다운스트림(클라이언트:고객)로 단방향 의존 표현
순응주의자(Conformist) : 업스트림(서버) is King
부패 방지 계층(Anticorruption Layer) : 변환을 통해서 다운스트림 컨텍스트 내 순수함을 지킴 (Adapter + Translator)
오픈 호스트 서비스(Open Host Service) : REST/API, RPC, Socket
발행된 언어(Published Language) : Json, XML, Byte
분리된 방법(Seprate Ways) : 의존 없음
큰 진흙공(Big ball of mud) : 똥덩어리
세 가지 컨텍스트를 매핑하기
before
문제점 공간
분리된 핵심 을 이용
after
해결책 공간
대부분 순응주의자 를 많이 사용하는 것 같다.
상호 의존이 걸리는 partnership 도 많은 것 같다.
개인적으로는 고객-공급자 가 좋다.
일반적으로 핵심 도메인은 다운스트림 이다.
업스트림에서 다운스트림으로 모델 복제는 Evil 이다.
[출처] https://www.codeproject.com/Articles/1158628/Domain-Driven-Design-What-You-Need-to-Know-About-
통합 전에 ACL를 통해서 변활할 시 최소한의 속성만 있으면 된다.
식별자와 액세스 컨텍스트의 통합
중요한 것은 애자일 프로젝트 컨택스트에 식별자 도메인이 침투하지 못하게 하는 것
협업 컨텍스트와 통합
결과적 일관성이 중요하다. - 이벤트 기반 아키텍쳐
그렇다면 상태관리를 해야한다.
enum DiscussionAvailability {
ADD_ON_NOT_ENABLED , NOT_REQUESTED , REQUESTED , READY ;
}
@Value
class Discussion {
DiscussionAvailiability availability ;
DiscussionDescriptor descriptor ;
...
}
class Product extends Entity {
private Discussion discussion ;
...
}
Product에서 Discussion을 사용할 시 READY 인 경우에만 가능
협업.Calendar -> 프로젝트관리.Scheduling
협업.Forum -> 프로젝트관리.Discussion
마무리
OHS, PL, ACL
IDDD 3장. 컨텍스트 맵 was originally published by MJ at DevOOOOOOOOP on May 05, 2018.
source : http://redutan.github.io/2018/05/05/IDDD-chapter03
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
스티브 잡스
DDD는 우리가 높은 품질의 소프트웨어 모델을 설계할 수 있도록 해준다.
DDD는 전략적인 동시에 전술적(DDD-Lite)인 모델링 도구
나도 DDD를 할 수 있을까?
학습 곡선이 있다!!
DDD의 가장 중심에 있는 원리
토의, 경청, 이해, 발견, 비즈니스의 가치
모든 지식을 중앙화하는 모든 것
DDD는 객체지향적 방법 으로 엔터프라이즈 애플리케이션을 해결하는 방법
DDD는 MSA를 지향한다. Monolithic은 지양한다.
시니어 개발자 : 이 조직은 내가 생각했던 것보다 파괴적 진보에 관심이 없더군요. 뭐 상관 없어요 나는 포기하지 않겠어요.
도메인 전문가 : 유비쿼터스 용어로 소통하자
내가 왜 DDD를 해야할까?
개발자 도메인 전문가
설계는 코드이며, 코드가 설계다. 설계는 어떻게 작동하는가다. - Uncle Bob도 비슷한 말을 함
옳은 소프트웨어 개발 접근 방식에 투자한다는 생각
DDD가 해줄 수 있는 일
도메인 전문가와 개발자는 가까운 거리에서 유비쿼터스 언어로 소통하면서 협업해야 한다.
기술보다는 도메인 가치가 우선이다.
도메인의 복잡성과 씨름하기
핵심 도메인과 서브 도메인
ex) 커머스 솔루션에서 핵심 도메인은 결제이며, 서브 도메인은 배송이다.
DDD를 통해서 복잡화하지 말고 단순화하라
Anemic Domain Model : 빈약한 도메인 모델. 속성만 표현하는 단순한 데이타 홀더. 보통 트랜잭션 스크립트 와 연결된다.
Rich Domain Model : 풍부한 도메인 모델. DDD를 통해서 도메인을 표현한 풍부한 행위를 가지는 모델
과거에는 객체 관계형 임피던스 부조화 때문에 Anemic Domain Model 을 많이 사용하게 되는데, 이제 ORM이 대중화 되어서 할만하다!!
왜 무기력(Anemic)증이 일어나는가?
절차적 사고방식의 익숙함
단순한 샘플코드를 참고
비주얼 베이직 탓 : 클릭하고 드래그 앤 드랍 하면 프로그램이 만들어짐 - 이건 좀 ….
Getter, Setter의 자바빈을 숨기는 여러 방법이 있었지만, 대부분 개발자는 그러려고 하지 않았거나, 왜 그렇게 해야 하는지 이해조차 하지 못했다. 온통 무기력증 이다.
일반적인 행위로 모든 상황(도메인 지식)을 커버하려 한다. 이것은 안티 도메인 모델이 된다.
코드로 말하면 Setter들의 향연이 벌어진다. 그저 데이터 홀더일 뿐 이다. -> 무기력증
뷰를 위한 모델은 Getter, Setter로 구성되는 것이 좋다.(DTO 패턴) 하지만 도메인 모델은 아니다.
p.67 p.69 코드를 보면서 반성하자.
DDD는 어떻게 하는가?
유비쿼터스 언어 : 반운디드 컨텍스트 내에서 공유된 언어. 도메인 전문가화 개발자 모두에 의해 개발되어 공유된 언어. - 서로 많이 이야기 하자.
ex) 간호사가 독감 백신을 표준 용량으로 환자에게 투여한다. : nurse.administerFluVaccine(patient, vaccine);
도메인 모델링 - 소프트웨어는 계속 진화한다. 모델 설계를 관리하지 말고 언제나 버릴 수 있어야 한다. 결국 코드가 설계 이다.
유비쿼터스 언어 용어집 만들기
도메인 정제 (결국 많은 이야기를 해야한다)
p.75 샘플로 도메인적 표현을 가지는 모델링을 생각해보자
가독성이 좋아졌다.
도메인의 표현이 보인다.
그리고 클라이언트 입장에서 테스트
DDD를 사용하는 데서 오는 비즈니스 가치
부제 : 여러분의 상사에게 DDD를 파는 방법
조직이 그 도메인에 유용한 모델을 얻는다.
정교하게 정확하게 비즈니스를 정의하고 이해한다.
도메인 전문가가 소프트웨어에 설계에 기여한다.
사용자 경험이 개선된다.
순수한 모델 주변에 명확한 경계가 생긴다.
엔터프라이즈 아키텍처의 구성이 좋아진다.
애자일하고, 반복적이고 지속적인 모델링이 사용된다.
전략적인 동시에 전술적인 새로운 도구가 적용된다.
DDD 적용의 난관
유비쿼터스 언어 만들기
도메인 전문가와 소통
개발자의 사고방식 전환 : 기술 보다는 도메인이 먼저다.
객체는 속성(데이터)이 중요한 것이 아니라 행동이 중요하다.
가장 중요한 것은 팀 내 문화와 DDD의 학습곡선 인 것 같다.
p.83을 보면 도메인 전문가와 친해지는 법이 나오는 게 진정 이런게 TIP 이다.
Example 백로그를 스프린트로 커밋한다.
Anemic Domain Model
backlogItem . setSprintId ( sprintId );
backlogItem . setStatus ( BacklogItemStatusType . COMMITED );
행위가 원자적이지 않으며, 데이터 의존적이며, 불변식이 깨질 수도 있다.
Rich Domain Model
backlogItem . commitTo ( sprint );
도메인의 표현이 드러나고 원자적이며, 행동이 캡슐화 되어 있으며, 도메인 모델 밖으로 로직이 세어 나가지 않는다.
위 상태에서 아래와 같은 추가사항이 생길 시 어떤 방식이 더 기민하게 반응할 수 있을까?
만약 백로그 항목이 이미 다른 스프린트로 커밋됐다면, 먼저 언커밋 해야한다. 커밋이 완료되면 이해 당사자에 알려라(도메인 이벤트)
도메인 모델의 합리화
어짜피 대부분의 엔터프라이즈 웹애플리케이션에서는 도메인 모델이 좋은 선택이라고 본다.
DDD는 무겁지 않다.
with TDD. 클라이언트 입장에서 도메인 모델을 구현하는 것은 큰 도움이 된다.
가상 + 약간의 현실
협업툴!!
가상의 프로로젝트를 진행하는 것으로 이야기를 풀어보자
사스오베이션, 그들의 제품과 DDD의 사용
콜랍오베이션 : SNS
프로젝트오베이션 : ITS
DDD-Lite(전술)를 이용함. 즉 바운디드 컨텍스트(전략)는 무시함.
마무리
복잡성 극복
트랜잭션 스크립트의 단점
Rich Domain Model 이 좋다.
DDD 약팔이
IDDD 1장. DDD를 시작하며 was originally published by MJ at DevOOOOOOOOP on April 27, 2018.
source : http://redutan.github.io/2018/04/27/IDDD-chapter01
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
큰그림
서브도메인과 바운디드 컨텍스트의 활용
온라인 쇼핑몰 도메인
전자상거래 시스템 : 상품 카탈로그, 주문 , 송장, 배송 서브도메인
재고관리 시스템 : 재고관리
외부 예측 시스템 : ?
제품 카탈로그, 주문, 송장, 배송 모델 등을 하나로 엮어서 복잡도가 높아지며 부정적인 결과를 초래
소프트웨어의 관심사를 도메인을 바탕으로 분명하게 분리시켜야 한다.
상호 의존성을 갖기 때문에, 서브도메인으로 나누지 않는다면 변화가 계속됨에 따라 훨씬 큰 부담 을 지게 된다.
일반적으로 하나의 바운디드 컨텍스트에 하나의 서브도메인이 있는 것이 좋다. : 재고관리는 좋아 보인다.
물론 DSL이 포함되어야 한다.
같은 언어일지라도 개념은 전혀 다를 수 있다.
핵심 도메인에 집중하기
핵심도메인 : 주문
지원 서브도메인 : 배송, 상품, 송장
범용 서브도메인 : 회원(인증과 권한)
결국 도메인 전문가와 소통으로 정제하는 것이 중요하다.
왜 전략적 설계가 엄청나게 필수적인가
협업 개념이 사용자 및 권한과 강한 결합을 가지는 것이 옳은가?
협업 개념에서는 회원은 그저 작성자 일 뿐이다.
그림 2.3 팀이 전략적 설계의 기초를 이해하지 못했고, 이는 공동 모델에서 개념들이 잘못 찢어지도록 했다. 점선 안의 문제가 되는 요소들이다.
협업 도구는 사용자의 역할에 집중해야지, 사용자가 누구며 수행권한이 부여된 행동이 무엇인지에 관심이 있어선 안 된다. 하지만 위에서는 뒤섞여 버렸다.
포럼 은 단지 토론을 게시하고 싶은 작성자 만 있으면 된다.
사용자와 권한은 협업 컨텍스트에서 독립돼야 한다.
Big ball of mud : 거대한 진흙공 - 빈약한 도메인으로 가득찬 모노리식 애플리케이션
이런 사용자와 권한 서브도메엔을 범용 서브도메인이라고 한다.
이윽고 팀이 비협업 개념의 또 다른 집합을 모델링하는 상황이 오면, 핵심 도메인은 더욱 불확실해진다.
풍부한 협업의 유비쿼터스 언어를 제대로 소스 코드에 반영하지 못한 채, 이를 은영준에 내포하고 있을 뿐인 모델을 만들고 말 수도 있다.
팀은 비즈니스 도메인과 그에 따른 서브데메인은 물론이고, 반드시 그들이 개발하고 있는 바운디드 컨텍스트도 제대로 이해해야한다.
이를 통해 전략적 설계를 가로 막는 비열한 적인 거대한 진흙공의 더러운 물을 막을 수 있다.
현실의 도메인과 서브도메인
문제점 공간과 해결책 공간
도메인은 문제점 공간(problem space)과 해결책 공간(solution space)을 모두 갖고 있다.
문제점 공간 : 새로운 핵심 도메인을 만들기 위한 전체 도메인의 일부 > 핵심 도메인과 서브 도메인의 조합
해결책 공간 : 해결책을 소프트웨어로 구현 > 바운디드 컨텍스트
서브 도메인을 1:1로 바운디드 컨텍스트로 묶는 것은 좋은 목표이다 하지만 항상 그렇지는 않다.
그림 2.4 구매나 재고관리와 관련된 핵심 도메인과 다른 서브도메인. 이 관점은 특정 문제점 공간 분석을 위해 선택된 서브도메인으로 제한되며 전체 도메인에 적용할 수는 없다.
최적 매입 컨텍스트 = 핵심 도메인
구매 컨텍스트 + ERP 구매 모듈 = 구매 지원 서브 도메인
재고 관리 컨텍스트 + ERP 재고 모듈 = 재고 관리 지원 서브 도메인
매핑 컨텍스트 : 범용 서브 도메인
최적 매입 컨텍스트를 개발하는 회사 입장에서의 이런 핵심 포인트 를 살펴봤음을 기억하자.
지리적 매핑 서비스는 문제점 공간에서 재고관리 서브도메인의 일부로 간주하지만, 해결책 공간에서 재고관리 컨텍스트가 아니다.
매핑 서비스가 해결책 공간에선 간단한 컴포넌트 기반 API로 제공됐다 하더라도, 이는 다른 바운디드 컨텍스트다.
재고 관리와 매핑의 유비쿼터스 언어는 상호 배타적이며, 이는 두 요소가 서로 다른 바운디드 컨텍스트라는 의미다.
재고 관리 컨텍스트가 외부 매핑 컨텍스트의 어떤 부분을 사용하는 상황에서, 데이터는 적어도 최소한의 변환 을 거쳐야만 적절히 사용될 수 있다.
한편 구독자를 위해 매핑 서비스를 개발 하고 제공하는 외부 비즈니스 조직의 관점에서 보면 매핑은 핵심 도메인 이다.
전략적 이니셔티브 : KPI를 달성하기 위한 핵심적인 활동이나 계획
바운디드 컨텍스트 이해하기
바운디드 컨텍스트는 그 안에 도메인 모델이 존재하는 명시적 경계
바운디드 컨텍스는 명시적이고 언어적이다
컨텍스트가 왕이다. in DDD
모델의 중앙화, 범용화는 Evil 이다. 컨텍스트 별로 모델은 다른 의미를 가진다.
그림 2.5 두 바인디드 컨텍스트 속 어카운트 객체는 의미가 서로 완전 다르지만, 각 바운디드 컨텍스트 안에서 고려해야만 그 사실을 알 수 있다.
컨텍스트
어카운트 의미
예
은행
계좌
적금 계좌
문학
이야기
A Personal Account of Mt. Everest Disaster
모든 것을 빠짐없이 포괄하는 모델을 생성하려 시도하는 함정에 빠지며, 어디서든 통용되는 유일한 의미를 가진 이름의 개념에 대해 전체 조직이 모두 동의하는 결과를 목표로 삼는다.
이런 모델링 접근법에는 구멍이 있다.
모든 이해관계자로부터 모든 개념이 하나의 순수하고 구분된 글로벌한 의미를 갖는 것에 대해 동의를 얻기란 거의 불가능하다.
모든 사람을 함께 모으는 것은 절대 불가능하다. : 이건 좀 아닌 것 같음.
최상의 선택을 위해선 언제나 차이점은 존재한다는 사실을 직시하고 ,바운디드 컨텍스트를 통해 차이점이 명확하며 잘 이해하고 있는 도메인 모델을 각각 기술해야 한다.
1 바운디드 컨텍스트 != 1 프로젝트 아티팩트(산출물)
주문은 카탈로그 컨텍스트와 주문 컨텍스트에서 다른 모델로 표현된다.
고객은 등록, 계정, 배송 컨텍스트 별로 다른 모델로 표현된다.
모델 그 이상을 위해
아래 항목들도 바운디드 컨텍스트 경계 안에 있다.
Application Service : 보안 트랜젝션 관리 : 퍼사드
UI
Api Client : API 통합
안티패턴
Smart UI
바운디드 컨텍스트의 크기
= 유비쿼터스 언어를 표현하기 위해 필요한 크기
딱 내가 원했던 만큼의 음표들이 있었습니다. 더도 덜도 아닌
모짜르트
잘못된 크기의 바운디드 컨텍스트를 만들게 되는 이유?
아키텍처적 영향을 기준으로 삼는 경우(플랫폼, 프레임워크, 컴포넌트, 인프라 등)
개발자 리소스(또는 팀)를 작업을 분배하기 위해
기술적 컴포넌트로 정렬하기
com.mycompany.optimalpuchasing : bounded context
com.mycompany.optimalpuchasing.prsentation : ui
com.mycompany.optimalpuchasing.application : application
com.mycompany.optimalpuchasing.domain.model : domain
com.mycompany.optimalpuchasing.infrastructure : infra
샘플 컨텍스트
그림 2.7 완전히 서브도메인과 정렬된 바운디드 컨텍스트 샘플의 평가 관점
출처 : https://www.safaribooksonline.com/library/view/implementing-domain-driven-design/9780133039900/ch02lev1sec5.html
협업 컨텍스트
before
public class Forum extends Entity {
public Discussion startDiscussion (...) {
// repository 의존
User user = userRepository . userFor ( this . tenantId (), aUsername );
// 보안 로직
if (! user . hasPermissionTo ( Permission . Foru . StartDiscussion ))
//...
// 열차 충돌
String authorName = user . person (). name (). asFormattedName ();
//...
return newDiscussion ;
}
}
협업 보다는 보안을 염두함 in 협업 컨텍스트
전술적 패턴(DDD-Lite)으로는 해결 불가능. 전략적 설계 컨텍스트 맵!!
열차사고
usr.person().name().asFormattedName()
디미터의 법칙 위배
결론적 해결책을 통해 추가적인 전략적 설계를 사용해, 재사용 가능한 모델을 별도의 바운디드 컨텍스트로 분리하고 적절히 통합할 수 있게 됐다
after
public class ForumService {
@Transactional
public Discussion startDiscussion (...) {
Author author = this . colloboratorSrvice . authorFrom ( tenant , anAuthorId );
Discussion new Discussion = forum . startDiscussionFor (...);
//...
return newDiscussion ;
}
}
public class Forum extends Entity {
public Discussion startDiscussionFor (...) {
//...
}
}
User 와 Permission 의 의존을 제거하고 모델을 엄격히 협업에만 집중하게 함 Author
식별자와 엑세스 컨텍스트
사일로 효과 : 연통 배관 : 상호 협력을 하지 않고 중복이 생기고 각각 따로 놀게됨
위에서 지적된 것 처럼 식별자 컨텍스트를 분리하고 범용 지원 도메인으로써 활용
애자일 프로젝트 관리 컨텍스트
DDD 전략적 설계를 바탕으로 애자일 프로젝트 컨텍스트를 분리
컨텍스트는 각 팀에게 아주 구체적인 의미를 부여한다.
마무리
도메인, 서브도메인, 바운디드 컨텍스트
문제점 공간, 해결책 공간
모델을 구분(User vs Author)
바운디드 컨텍스트의 크기
사스오베이션 팀의 바운디드 컨텍스트 정제
IDDD 2장. 도메인, 서브도메인, 바운디드 컨텍스트 was originally published by MJ at DevOOOOOOOOP on April 28, 2018.
source : http://redutan.github.io/2018/04/28/IDDD-chapter02
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW
동기
사전지식
LSP
Practice
Summary
동기
어느 날이었습니다. 팀 내에서 상속에 대한 이야기를 하다가 주니어가 그럼 상속은 어떻게 써야하는지 궁금해 했습니다.
그런데 이 상속의 위험성을 설명하려고 하니, 말로만 하기에는 부족한 것 같고, 그렇다면 worst-case 코드를 보여줘야 하는데 시간이 없었습니다.
그래서 상속의 위험성에 대한 소스코드와 그것을 설명하는 블로그 아티클을 작성해야겠다는 생각이 들었습니다.
사전지식
Java 언어를 기본으로 설명합니다.
가변과 불변
객체지향은 가변(mutable)을 캡슐화(또는 관리)해서 복잡성을 제어합니다.
하지만 근본적으로 가변은 부수효과(side-effect)를 동반합니다.
그래서 가능하면 가변을 최소화 하는 것이 유리합니다.
이를 해결하기 위해서 불변(immutable)을 이용하는 것도 좋은 방법입니다.
접근제어자
public : 모두 접근 가능
protected : 자식 클래스와 같은 패키지 상에서 접근 가능
default(package) : 같은 패키지 상에서 접근 가능
private : 클래스 내에서만 접근 가능
public , protected 는 열려 있으며, default , private 는 닫혀 있다. - Joshua bloch
접근제어는 가능한 닫혀 있는 것이 좋습니다.
public field는 캡슐화되지 않으므로 Evil 으로 규정합니다.
불변식(불변조건)
클래스 불변식(Class Invariant)은 해당 클래스의 오브젝트가 가지는 제약사항을 말합니다.
즉 불변식이 깨지면 해당 객체는 유효하지 않다고 봐야하며 , 애플리케이션 내 클래스의 계약을 위배했으므로, 문제를 발생시킵니다.
예를 들어서 분수를 나타내는 클래스가 있다고 가정해 보겠습니다.
class 분수 {
public int 분자 ;
public int 분모 ;
@Override
public String toString () {
return 분자 + "/" + 분모 ;
}
}
class 분수 Test {
@Test
public void test 분수 _invalid () {
분수 분수객체 = new 분수 ();
분수객체 . 분자 = 1 ;
분수객체 . 분모 = 0 ; // !!! 분모는 0이 아니여야함 (불변식이 깨짐)
}
}
내부 필드가 public이기 때문에 캡슐화를 통해서 불변식을 강제할 수가 없습니다.
불변식 제약사항을 강제하는 메소드를 재정의함으로써도 깨질 수도 있습니다.
LSP
서브타입(sub-type)은 그것의 기반 타입(base-type)으로 치환 가능해야 한다.
그냥 단순하게 기반 타입으로 치환만 된다는 것을 의미하지는 않습니다. 기반 타입의 행위들을 서브 타입의 행위들로 대치해도 문제가 없고, 불변식도 깨지지 않아야 함을 의미합니다.
LSP를 자체를 설명하기 보다는 LSP가 위배되는 상황을 통해서 역으로 LSP를 알아보겠습니다.
유명한 Rectangle(직사각형) - Square(정사각형) 예제를 통해서 이를 확인해 보겠습니다.
class Rectangle {
private int width ;
private int height ;
public void setWidth ( int width ) {
this . width = width ;
}
public void setHeight ( int height ) {
this . height = height ;
}
public final int getArea () {
return width * height ;
}
}
class Square extends Rectangle {
@Override
public void setWidth ( int width ) {
super . setWidth ( width );
super . setHeight ( height );
}
@Override
public void setHeight ( int height ) {
this . setWidth ( height );
}
}
위 정도면 충분히 LSP 를 만족한다고 보입니다. 과연 그럴까요?
먼저 Square 클래스의 불변식을 알아봅시다. 정사각형이기 때문에 길이와 높이가 같은 것이 불변식입니다.
Rectangle 는 어떤 불변식을 가질까요? 길이와 높이가 무조건 같이 변경되면 직사각형의 불변식이 위배됩니다. - 두 타입 간 충돌이 발생하는 느낌도 있습니다.
즉, Square 는 길이와 높이를 무조건 같이 변경하게 되지만, Rectangle 는 길이와 높이가 같이 변경되면 예상치 못한 부수효과로 인해 불변식이 깨지게 됩니다.
Rectangle 의 불변식이 깨지게 됨은 상위 타입으로 치환이 불가능하다로 연결되므로 LSP도 위배하게 됩니다.
물론 불변식이 깨진다고 해서 무조건 LSP가 위배되는 것은 아닙니다.
Solution
먼저 상속을 유지한 상태에서 해결 방안을 알아보겠습니다.
class Retangle {
private final int width ;
private final int height ;
public Rectangle ( int width , int height ) {
this . width = width ;
this . height = height ;
}
public final int getArea () {
return width * height ;
}
}
class Square extends Rectangle {
public Square ( int length ) {
super ( length , length );
}
}
부수효과는 가변메서드(Setter)에서 발생합니다. 그럼 애초에 원인이 되는 가변을 모두 제거해서 위와 같이 불변(Immutable)을 통해서 문제를 해결 할 수 있습니다.
Another Solution
다른 방법을 알아볼까요?
애초에 이 애플리케이션 세계에서 사각형과 정사각형은 상속구조가 어울리지 않는 것 같습니다.
interface Shape {
int getArea ();
}
final class Rectangle implements Shape {
private final int width ;
private final int height ;
public Rectangle ( int width , int height ) {
this . width = width ;
this . height = height ;
}
@Override
public int getArea () {
return width * height ;
}
}
final class Square implements Shape {
private Rectangle target ;
public Square ( int length ) {
setLength ();
}
public void setLength ( int length ) {
this . target = new Rectangle ( length , length );
}
@Override
public int getArea () {
return target . getArea ();
}
}
상속 보다는 합성(Composition) 원칙에 입각해서 위와 같이 수정하는 것도 한 방법입니다.
실제로 중요한 것은 넓이를 구하는 행위이지 사각형이냐, 정사각형이냐는 그 다음 문제입니다.
또한 합성을 이용하면 Setter(‘setLength’)가 있더라도 불변식이 깨지는 부수효과가 발생하지 않습니다.
Practice
아래 다양한 예시를 통해서 더 안전한 상속을 구현하는 방법을 알아보겠습니다.
메서드 재정의
메서드가 재정의 불가능하게 final 로 닫는 것이 좋습니다.
Bad
base : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/override/BadSuperObject.java
Good
https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/override/GoodSuperObject.java
Support 타입
상속을 단순 코드 재사용으로 사용하는 경우(추상 메서드가 없는 경우)에는 합성(Composition)을 사용하는 것이 좋습니다.
Bad
base : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/support/ProcessSupport.java
sub : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/support/BadMainProcess.java
Good
helper : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/support/ProcessHelper.java
client : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/support/GoodMainProcess.java
Template
템플릿 메소드 패턴의 경우 아래와 같은 코드로 정형화 하는 것이 좋습니다. - 이것은 상속을 이용한 Good Practice 중 하나입니다.
템플릿 패턴은 변하는 부분과 변하지 않는 부분의 관심사 분리가 중요합니다.
변하는 부분은 다형성을 위해 열어두고 변하지 않는 부분은 불변 템플릿(final)으로 만듭니다.
Good Sample 중 일부 코드
public abstract class AbstractSafePrefixContentHolder implements ContentHolder {
// 가능한 필드는 닫고 불변화 시킨다. 접근이 필요할 때만 점진적으로 연다.
private final String content ;
public AbstractSafePrefixContentHolder ( String content ) {
this . content = Objects . requireNonNull ( content ); // 여기에서 제약조건을 추가할 수 있다. : 선행조건으로 불변식 강제
}
@Override // 템플릿 : 재정의 불가능하게 final
public final String getContent () {
return getPrefix () + content ;
}
// 다형성으로써 추상 메서드만 오픈시킨다.
abstract protected String getPrefix ();
}
Bad
base : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/template/AbstractPrefixContentHolder.java
sub : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/template/BadContentHolder.java
Good
base : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/template/AbstractSafePrefixContentHolder.java
sub : https://github.com/redutan/dangers-of-inheritance/blob/master/src/main/java/io/redutan/dangers/inheritance/template/GoodContentHolder.java
Summary
불변식을 지킵니다.
접근제어는 가능한 닫습니다 : field는 private 로
가능한 변경을 최소화 합니다 : final
불변을 이용하거나, 인터페이스를 통한 합성으로 변경해 봅니다.
예외
하지만 모든 경우에서 위 원칙을 지키는 것은 힘들수도 있습니다.
상속의 위험성을 모두 파악한 상태에서 문서(javadoc)를 통해서 제약사항을 명시해서 언어로써 강제하는 것이 아니라 프로그래머가 스스로 제약사항을 지키게 하는 것도 한 방법입니다.
http://redutan.github.io/2016/02/26/effective-java2-chapter04#rule-17—계승을-위한-설계와-문서를-갖추거나-그럴-수-없다면-계승을-금지하라
그리고 특정 도메인(ex:환경설정)에서는 위 상속의 위험성을 무시할 수도 있습니다.
Reference
github : https://github.com/redutan/dangers-of-inheritance/
http://redutan.github.io/2016/02/26/effective-java2-chapter04
http://redutan.github.io/2017/06/10/clean-software-part02-2
https://en.wikipedia.org/wiki/Class_invariant
https://ko.wikipedia.org/wiki/리스코프_치환_원칙
상속의 위험성 was originally published by MJ at DevOOOOOOOOP on April 21, 2018.
source : http://redutan.github.io/2018/04/21/dangers-of-inheritance
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW