IDDD 14장. 애플리케이션

사용자 인퍼페이스와 도메인 모델 사이에 위치

유스케이스 태스크를 조정

트랜잭션, 보안 권한 부여 담당

애플리케이션을 핵심 도메인 모델과 상호 교류하며 이를 지원하기 위해 잘 조합된 컴포넌트의 집합을 의미하기 위해 사용하고 있다.

이는 일반적으로 도메인 모델 그 자체와 사용자 인터페이스, 내부적으로 사용되는 애플리케이션 서비스, 인프라적 컴포넌트를 뜻한다.

이 각각의 영역에 어떤 것들이 들어가는지는 애플리케이션마다 다르며, 사용하는 아키텍처가 무엇인지에 따라서도 달라진다.

그림 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

IDDD 7장. 서비스

도메인 서비스 : 도메인 모델 내 무상태 오퍼레이션 제공 애그리게잇이나 값 객체의 행위로써 어울리지 않는 것 before class Product { Set backlogItems ; BusinessPriorityTotals businessPriorityTotals () { //... } } 모델 정제를 통해 Set backlogItems 를 분리 그렇다면 businessPriorityTotals() 메서드는 어떻게 되는가? after class Product { static BusinessPriorityTotals businessPriorityTotals ( Set aBacklogItems ) { // ... } } 정적 메소드로 변경 - 하지만 과연 이것은 옳은가? 도메인 서비스 가 필요한 시점이다! 도메인 서비스란 무엇인가 하지만 그보다 먼저 도메인 서비스가 아닌 것은 무엇인가? 도메인 서비스 : 도메인 로직이 있다. 애플리케이션 서비스 : 도메인 로직이 없다. (보안, 트랜잭션 등) 정의 엔터티나 값객체의 책임이 아닌 (도메인적) 행위 유비쿼터스 언어로써 표현 무상태! 예시 중요 비즈니스 프로세스 복수 개 의 도메인 객체에서 필요로 하는 계산 한 조합에서 다른 조합으로 도메인 객체를 변형 할 때 서비스가 필요할지 확인하자 도메인 특화 지식은 클라이언트로 절대 유촐돼선 안된다.(클라이언트가 애플리케이션 서비스일지라도) 도메인 객체에 위치 시키기 애매하면 도메인 서비스 에 담는다. 주의 : 도메인 서비스를 남용하면 빈약한(Anemic) 도메인 모델이 된다. 도메인에서 서비스를 모델링하기 AuthenticationService package com . saasovation . identityaccess . domain . model . identity ; // !! interface AuthenticationService { UserDescriptor authenticate ( TenantId tenantId , String username , Strin password ); } EncryptionAuthenticationService package com . saasovation . identityaccess . infra . services ; // !! class EncryptionAuthenticationService implements AuthenticationService { @Override UserDescriptor authenticate ( @NonNull TenantId tenantId , @NonNull String username , @NonNull Strin password ) { Tenant tenant = tenantRepository . findById ( tenantId ); if ( tenant == null || ! tenant . isActive ()) { throw new TenantNotFoundException ( tenantId ); } String encryptedPassword = encryptService . encryptedValue ( passwod ); // 암호화 책임도 도메인 서비스? User user = userRepository . findByTenantAndUsernameAndPassword ( tenant , username , encryptedPassword ); if ( user == null || ! user . isEnabled ()) { throw new UserNotFoundException ( username ); } return user . userDescriptor (); } } 분리된 인터페이스가 꼭 필요할까 다형성이 요구되지 않는다면 굳이 분리된 인터페이스는 필요하지 않다고 본다. 다형성 등으로 인한 추상화가 요구될 때 분리된 인터페이스로 리팩토링 하자 생각 : 구현클래스에 Impl 접미사를 붙이는 것은 가능하면 피하는 것이 좋다고 본다. DI 프레임워크(ex:Spring)를 사용하면 분리된 인터페이스를 사용하기 더 편하다. 계산 프로세스 생각 : 계산(연산)은 변하지 않는 행동이므로 분리된 인터페이스 는 필요하지 않다. 대부분 경우 계산 프로세스를 정적 메서드 로 해결하는 경향이 많다. 이는 잘못된 행동이며 계산이 도메인 로직을 표현한다면 당연히 도메인 모델 내에 도메인 서비스로써 존재 해야 한다. 참고 : p.369 ~ 371 소스코드 변환 서비스 타 서비스와 통합을 위해 사용 이것도 도메인 서비스 였다니 Adapter package com . saasovation . collaboration . infra . services ; class UserInRoleAdapter { T toCollaborator ( Tenant tenant , String identity , String role , Class collaboratorClass ); } Translator package com . sasovation . collaboration . infra . services ; class CollbaratorTransaltor { T toCollaboratorFrom ( String json , Class collaboratorClass ); } 생각 : translator도 도메인 서비스 인가? 도메인 서비스의 미니 계층 사용하기 가능하면 미니 계층은 사용하지 말자 - 안티패턴! 하지만 필요하다면 특정 도메인만 제한적으로 사용하는 것도 좋다. 마무리 도메인 서비스는 필요할 때 사용 남용하면 빈약한 도메인 모델화 무상태 VS 애플리케이션 서비스 IDDD 7장. 서비스 was originally published by MJ at DevOOOOOOOOP on May 18, 2018. source : http://redutan.github.io/2018/05/18/IDDD-chapter07
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW

IDDD 6장. 값 객체

가능한 엔터티 대신 값 객체를 사용해 모델링하도록 노력해야 한다.

특성만 신경 쓴다면 이를 값 객체로 분리하라. 특성의 의미를 표현하고 기능도 부여하자. 불변성. 식별자는 필요 없고, 설계의 복잡성을 줄여준다.

값의 특징

개념을 값으로

측정, 수량화, 설명

불변성(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

IDDD 12장. 리파지토리

전역(global) 액세스가 필요한 각 객체의 타입이다, 해당 타입의 모든 객체가 담긴 인메모리 컬렉션이란 허상을 제공하는 객체를 생성하자.

잘 알려진 전역의 인터페이스를 통한 액세스를 설정하자.

객체를 추가하거나 제거하는 메소드를 제공하자…

일정 조건에 부합되는 특성을 갖는 객체를 선택해 완전히 인스턴스화된 객체나 여러 객체의 컬렉션으로 반환하는 메소드를 제공하자…

애그리게잇에 대해서만 리파지토리를 제공하자.. [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

IDDD 111장. 팩토리

애그리게잇을 생성하는 책임을 가지는 메소드나 객체를 말한다.

애그리게잇 생성을 캡슐화

도메인 모댈 내의 팩토리

복잡한 객체와 애그리게잇 인스턴스를 생성하는 책임을 별도의 객체로 이동시키자. 여기서의 책임은 도메인 모델과 관련이 있지 않지만, 여전히 도메인 설계를 구성하는 한 요소 다. 모든 복잡한 조립 과정을 캡슐화하고, 클라이언트가 인스턴스화된 객체의 구체적 클래스를 참조할 필요가 없도록 인터페이스를 제공하자. 전체 애그리게잇을 하나의 조각(원자성)으로 생성하고 고정자(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

IDDD 10장. 애그리게잇

일관성 경계 내에서 엔터티와 값 객체의 묶음

일관성 경계 의 기준은 같은 트랜잭션인가로 검증된다.

애그리게잇 내의 불변식(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

IDDD 9장. 모듈

Java: 패키지

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

IDDD 8장. 도메인 이벤트

Publish-Subscribe

언제 그리고 왜 도메인 이벤트를 사용할까?

도메인 내 어떤 사건이 발생했을 때

한 트랜잭션에는 한 애그리게잇만 커밋

이벤트의 모델링

어떤 명령에서 어떤 사건이 발생했었음

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

IDDD 4장. 아키텍처

실제 요구(도메인)가 아키텍처 스타일과 패턴의 사용을 유도해야 한다.

도메인이 기술 보다 먼저다.

성공한 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