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

  • 일관성 경계의 기준은 같은 트랜잭션인가로 검증된다.
  • 애그리게잇 내의 불변식(invariant)?

스크럼 핵심 도메인에서 애그리게잇 사용하기

기능 목록

  • 제품은 백로그 아이템과 릴리스, 스프린트를 포함한다.
  • 새로운 제품 백로그 아이템을 계획했다.
  • 새로운 제품 릴리스를 계획했다.
  • 새로운 제품 스프린트 일정을 수립했다.
  • 계획된 백로그 아이템에 관한 릴리스 일정을 수립할 수 있다.
  • 일정이 잡힌 백로그 아이템은 스프린트로 커밋할 수 있다.

첫 번째 시도: 큰 클러스터의 애그리게잇

제품이 ~를 포함한다.

컴포지션 VS 객체 그래프 포함 VS (상호) 연결

도메인 로직

  • 백로그 항목을 스프린트로 커밋하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
  • 스프린트가 백로그 항목을 커밋하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
  • 릴리스가 백로그 항목의 일정을 수립하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
  • 백로그 항목의 릴리스 일정을 수립하면, 이를 시스템에서 제거하도록 허용해선 안 된다.
class Product {
    private Set<BacklogItem> backlogItems;
    private String description;
    private String name;
    private ProductId productId;
    private Set<Release> releases;
    private Set<Spring> sprints;
    private TenantId tenantId;
}

아주 큰 애그리게잇으로 모델링된 Product

아주 큰 애그리게잇으로 모델링된 Product

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` 애그리게잇

Product 모델에선 다양한 기본 오퍼레이션이 수행될 동안 큰 컬랙션을 여럿 가져오게 된다.

  • 이 큰 클러스터 애그리게잇은 성능이나 확장성이 절대로 좋을 수 없다. 이는 실패로 이어지는 악몽이 될 뿐이다. 거짓 고정자와 컴포지션적 편의성이 설계를 주도했기 때문에 시작부터 문제가 있었으며, 트랜잭션의 종료, 성능, 확장성의 측면에서 안 좋은 영향을 미쳤다.

작은 애그리게잇은? 다른 대상과 일관성을 유지

  • 변경이 되면 엔터티
  • 대치가 되면 값 객체 : 생각보다 상당히 많은 개념이 값 객체로 대치된다.

파생 금융상품 부문에서 약 70% 애그리게잇이 단 하나의 루트 엔터티로 구성된다.

작은 애그리게잇은

  • 성능이 좋음
  • 확장성이 좋음
  • 트랜잭션이 성공할 가능성이 크다

유스케이스를 전부 믿지는 말라

  • 하나의 유스케이스가 여러 트랜잭션을 발생 시킨다면 의심해 보자
    • 이런 경우에서 결과적 일관성을 통해서 문제를 해결할 수 있다. + 지연 업데이트
  • 물론 하나의 유스케이스가 하나의 트랜잭션일 필요는 없다.

규칙: ID로 다른 애그리게잇을 참조하라

객체 그래프가 연결되어 있다고 해서 같은 애그리게잇은 아니다. 그저 다른 애그리게잇을 연결했을 뿐이다.

여기도 결국 결과적 일관성으로 이어진다.

애그리게잇 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(Implementing Domain-Driven Design) 스터디 중 값 객체 Collection을 ORM(hibernate)을 이용하여 구현하는 예제를 확인했습니다. 그러던 중 이게 아주 과거 hibernate xml 구성 기준이어서 현재 JPA(Sprin Data JPA)에서는 어떻게 구현되는지 궁금해 하던 @동묘가 저에게 직접 구현을 보고 싶다고 해서 포스팅 하게 되었습니다.

구현

3가지 구현 방법

  1. Single Column
  2. Entity
  3. Join Table

예시 도메인

Group과 GroupMember

  • 그룹은 애그리게잇 루트(엔터티)입니다.
  • 그룹맴버는 값 객체입니다.
  • 한 그룹에는 여러 그룹맴버가 있습니다. (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<GroupMember> group1Members = new HashSet<>();
    ...
}

GroupMembersConverter.java

@Converter
public class GroupMembersConverter implements AttributeConverter<Set<GroupMember>, String> {
    private ObjectMapper om = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Set<GroupMember> attribute) {
        return om.writeValueAsString(attribute);
    }

    @Override
    public Set<GroupMember> convertToEntityAttribute(String dbData) {
        return om.readValue(dbData, new TypeReference<Set<GroupMember>>() { });
    }
}

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 (
    group_id, description, group_members, name
) values (
    ?, ?, ?, ?
);

저장 후 테이블 조회 예시

GROUP_ID DESCRIPTION GROUP_MEMBERS NAME
1 설명 [{"name":"하위그룹","type":"GROUP"},{"name":"회원1","type":"MEMBER"}] 이름

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<GroupMember> 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 (group_id, description, name) 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<GroupMember> 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 (group_id, description, name) 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<GroupMember> groupMembers = new HashSet<>();
    ...
}

Summary

Total DB schema

  1. Single Column
  2. Entity
  3. Join Table

Reference

  • Java: 패키지
  • C#: 네임스페이스
  • Ruby: 모듈

모듈로 설계하기

  • 모듈 : 도메인 객체의 컨테이너

규칙

  • 모델링의 개념에 맞춰 모듈을 설계하자
  • 유비쿼터스 언어에 맞춰 모듈을 명명하자
  • 모델에서 사용하는 일반적인 컴포넌트 타입이나 패턴에 따라서 기계적으로 모듈을 생성하지 말자
  • 느슨하게 결합된 모듈을 설계하자.
  • 결합이 필요하다면 짝이 되는 모듈 사이에서 비순환적 의존성이 형성되도록 노력하자
  • 자식 모듈과 부모 모듈 사이에 규칙은 느슨하게 하자
  • 모듈을 모델의 정적인 개념에 따라 만들지 말고, 모듈이 담고 있는 객체에 맞추도록 하자

주방의 서랍에 식기류가 포크와 나이프와 스푼별로 잘 정리돼 있음

기본 모듈 명명 규칙

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)으로 설계하게된다. 모듈 내의 컴포넌트들에 따라서 나눠질 수도 합쳐질 수도 있다. 중요한 것은 도메인 로직과 유비쿼터스 언어에 따르는 것이다.

마무리

  • 유비쿼터스 언어를 표현한다.
  • 모듈 설계의 예시!!
  • 기계적인 모듈 설계는 창의성을 방해한다.
  • 바운디드 컨텍스트 분리보다 모듈 사용이 먼저다.

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

발행자

발행자

class BacklogItem extends AbstractAggregateRoot<BacklogItem> {
    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 구독 후 처리
    }
}

중요한 것은 결과적 일관성

뉴스를 원격 바운디드 컨텍스트로 전파하기

시스템 간 결과적 일관성 확보

메시징 인프라의 일관성

구현방법

  1. 도메인 모델과 메시지 인프라 저장소 공유
  2. 원격 DB with XA
  3. 이벤트 저장소

자치 서비스와 시스템

자치 서비스 : 이벤트를 통해서 시스템 간 결합도(독립성!)를 줄이는 기법 (No-RPC)

자치 서비스의 단위는 바운디드 컨텍스트가 되면 좋은 것 같다.

지연 시간 허용

결과적 일관성을 위해서

도메인 별로 그 때 그 때 달라요.

시스템의 허용치를 만족시키면서도 잘 수행되도록 아키텍처 품질을 높여야 한다.

이벤트 저장소

이벤트의 상태를 유지하기 위해서 저장하는 것이 요구되는 경우가 많다.

IdentityAccessEventProcessor

@Aspect
class IdentityAccessEventProcessor {
    @Before("execution(* com.saasovation.identityaccess.application.*.**..))")
    public void listen() {
        DomainEventPublisher.instance()
            .subscribe(new DomainEventSubscriber<DomainEvent>() {
                public void handleEvent(DomainEvent aDomainEvent) {
                    store(aDomainEvent);
                }

                public Class<DomainEvent> 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)
)

저장된 이벤트의 전달을 위한 아키텍처 스타일

레스트품 리소스로서 알림 발행하기

  1. 이벤트를 REST/WEB API로 발행한다. (이벤트 아이디나 애그리게잇 아이디를 전달)
    • 이벤트 저장소를 통해서 조회하거나, 애그리게잇을 직접 조회한다.
  2. 구독 측에서 발행측에 다시 상세 이벤트 정보를 조회한 후 처리한다.

메시징 미들웨어를 통한 알림 발행

  1. 메시징 미들웨어를 통해서 이벤트를 발행한다.
  2. 메시징 미들웨어를 구독한 구독 측에서 발행 측의 상세 이벤트 정보를 조회한 후 처리 한다.

발행과 구독은 exchange나 queue 개념으로 연결된다.

구현

Skip

  • 멱등 수신자 처리가 중요하다 : At least once

멱등성 : 오퍼레이션이 두 번 이상 수행되어도, 한 번만 수행했을 때와 같은 결과에 이르는 동작을 의미

궁극적으로 이벤트를 추적해야하는 경우가 생기기 때문에 이벤트 저장 기능이 거의 필수적으로 요구된다. 그리고 추적 일관성을 위해서 이벤트 저장은 도메인 로직과 트랜잭션으로 묶이는 것이 중요하다.

마무리

  • 이벤트 만의 고유 식별자가 요구된다.
    • 이벤트 저장소도 필요
  • 어짜피 발행-구독!
  • 멱등성 또는 중복 발행 or 수신 제거 확보

도메인 서비스 : 도메인 모델 내 무상태 오퍼레이션 제공

  • 애그리게잇이나 값 객체의 행위로써 어울리지 않는 것

before

class Product {
    Set<BacklogItem> backlogItems;

    BusinessPriorityTotals businessPriorityTotals() {
        //...
    }
}
  • 모델 정제를 통해 Set<BacklogItem> backlogItems를 분리
  • 그렇다면 businessPriorityTotals() 메서드는 어떻게 되는가?

after

class Product {
    static BusinessPriorityTotals businessPriorityTotals(
            Set<BacklogItem> 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 extends Collaborator> T toCollaborator(
            Tenant tenant, String identity, String role, 
            Class<T> collaboratorClass);
}

Translator

package com.sasovation.collaboration.infra.services;

class CollbaratorTransaltor {
    <T extends Collaborator> T toCollaboratorFrom(
            String json, Class<T> collaboratorClass);
}

생각: translator도 도메인 서비스 인가?

도메인 서비스의 미니 계층 사용하기

가능하면 미니 계층은 사용하지 말자 - 안티패턴!

  • 하지만 필요하다면 특정 도메인만 제한적으로 사용하는 것도 좋다.

마무리

  • 도메인 서비스는 필요할 때 사용
    • 남용하면 빈약한 도메인 모델화
  • 무상태
  • VS 애플리케이션 서비스