Skip to content

Instantly share code, notes, and snippets.

@QuadFlask
Last active March 22, 2020 05:07
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QuadFlask/3ac8a47e9a1b0673f8f199d5b020d95d to your computer and use it in GitHub Desktop.
Save QuadFlask/3ac8a47e9a1b0673f8f199d5b020d95d to your computer and use it in GitHub Desktop.

DDD start 책 공부

[ 유저 ] * - * [ 프로젝트 ] (n:m) 이 아니라

[ 유저 ] 1 - 1 [ 프로젝트맴버 * 프로젝트 ] 이렇게 도메인을 구분시키면 의존성도 낮추고 도메인도 구분되게 할 수 있음.


Chapter 3 애그리거트

ID를 이용한 애그리거트 참조

한 객체가 다른 객체를 참조하는것 처럼 애그리거트도 다른 애그리거트를 참조한다. 애그리거트의 관리 주체가 애그리거트 루트이므로 애그리거트에서 다른 애그리거트를 참조한다는것은 루트를 참조한다는것.

애그리거트간 참조는 필드를 통해서 쉽게 구현할 수 있다.

public class Member {}
public class Orderer {
	private Member member;
}

필드를 통해서 다른 애그리거트를 직접 참조하는것은 구현의 편리함이 있다. JPA를 사용하면 @OneToMany, @OneToOne같은 어노테이션으로 연관된 객체를 로딩하는 기능을 제공하므로 필드를 이용해서 다른 애그리거트를 쉽게 참조 할 수 있다.

하지만 필드를 이용한 애그리거트 참조는 아래 문제점이 있다

  • 편한 탐색 오용
  • 성능에 대한 고민
  • 확장 어려움

한 애그리거트 내부에서 다른 애그리거트의 객체를 쉽게 접근할 수 있으므로 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다. 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다. 다른 애그리거트의 상태를 변경하는것은 애그리거트간의 의존 결합도를 높여서 결과적으로 애그리거트의 변경을 어렵게 만든다.

성능은 lazy loading, eager loading

단일 DBMS로 서비스를 제공하다가 하위 도메인별로 시스템을 분리하면서 도메인마다 서로다른 DBMS를 사용하면 더이상 다른 애그리거트 루트를 참조하기 위해 JPA같은 단일 기술을 사용할 수 없다.

그래서 ID를 이용해서 다른 애그리거트를 참조하면 좋다.

class Member { 
	private MemberId id;
}
class Orderer {
	private MemberId memberId;
}

DB테이블에서의 외래키를 사용해서 참조하는 것과 비슷하게 다른 애그리거트를 참조할때 ID참조를 사용한다는 점. 단 애그리거트 내의 엔티티를 참조할때는 객체 레퍼런스로 참조한다.

이렇게 하면 모든객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명확히 하고 애그리거트간 물리적인 연결을 제거하기 때문에 복잡도를 낮춰준다. 또 애그리거트간 의존을 제거하므로 응집도를 높여준다. 또 구현 복잡도도 낮아진다. 직접 참조하지 않으므로 참조하는 애그리거트가 필요하면 응용서비스에서 아이디를 이용해서 로딩하면 된다. 직접 참조하지 않기 때문에 애그리거트 내부에서 다른 애그리거트의 상태를 변경할 수 없다. 애그리거트별로 다른 기술을 사용하는것도 가능하다.

이렇게 아이디를 이용한 참조를 하게되면 조인쿼리로 한번에 가져올 수 있는것을 N+1쿼리를 날려야 되서 성능이 후지게 된다. 이때 조회 전용 쿼리를 사용하면 된다. 데이터 조회를 위한 별도 DAO를 만들고 DAO의 조회 매서드에서 세타 조인(?)을 이용해서 한 번의 쿼리로 필요한 데이터를 로딩한다.

애그리거트 간 집합 연관

애그리거트를 팩토리로 사용하기

상점 계정이 차단 상태가 아닌 경우에만 상품을 생성하도록 구현할때

public class RegisterProductService {
	public ProductId registerNewProduct(NewProductRequest req) {
		Store account = accountRepository.findStoreById(req.getStoreId());
		checkNull(account);
		if (!account.isBlocked()) {
			throw new StoreBlockException();
		}
		productId id = productRepository.nextId();
		Product product = new Product(id, account.getId, ....);
		productRepository.save(product);
		return id;
	}
}

Product를 생성 가능한지 판단하는 코드와 Product를 생성하는 코드가 분리되어 있다. 중요한 도메인 로직 처리가 응용 서비스에 노출되어 있다. Store 가 Product를 생성할 수 있는지 여부를 판단하고 Product를 생성하는 것은 논리적으로 하나의 도메인 기능인데 이 도메인 기능을 응용 서비스에서 구현하고 있는것이다. 이 도메인 기능을 넣기 위한 별도의 도메인 서비스나 팩토리 클래스를 만들수도 있지만 이기능을 구현하기 더 좋은 장소는 Store 애그리거트이다.

public Store extends Member {
	public Product createProduct(ProductId productId) {
		if (isBlocked()) throw new StoreBlockedException();
		return new Product(newProduct, getId, .....);
	}
}

Store 애그리거트의 createProduct()는 Product 애그리거트를 생성하는 팩토리 역할을 한다. 팩토리 역할을 하면서도 중요한 도메인 로직을 구현하고 있다. 이제 응용 서비스는 팩토리 기능을 이용해서 Product를 생성하면 된다.

public class RegisterProductService {
	public ProductId registerNewProduct(NewProductRequest req) {
		Store account = accountRepository.findStoreById(req.getStoreId());
		checkNull(account);
		ProductId id = productRepository.nextId();
		Product product = account.createProduct(id, ...);
		productRepository.save(product);
		return id;
	}
}

앞선 코드와 차이점은 응용 서비스에서 더이상 Store의 상태를 확인하지 않는다는것이다. Store가 Product를 생성할 수 있는지 여부를 확인하는 도메인 로직은 Store에서 구현하고 있다. 이제 Product 생성 가능 여부를 확인하는 도메인 로직을 변경해도 도메인 영역의 Store 만 변경하면 되고 응용 서비스는 영향을 받지 않는다. 도메인의 응집도도 높아졌다.

애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야한다면 애그리거트에 팩토리 매서드를 구현하는것을 고려해보자.


Chapter 4 리포지터리와 모델 구현

  • 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고 리포지터리를 구현한 클래스는 인프라스트럭쳐 영역에 속함.

  • 매핑 구현 -> 엔티티와 벨류 기본 매핑 구현 -> 애그리거트 루트는 엔티티임. @Entity로 매핑. -> 한 테이블에 엔티티와 밸류 데이터가 있다면 밸류는 @Embeddable로 매핑. 밸류 타입 프로퍼티는 @Embedded로 설정

ex) Order는 주문 애그리거트의 루트 엔티티. 이 안에 Orderer, ShippingInfo는 밸류. 이것들은 한 테이블에 매핑 할 수 있음

Order.java

@Entiry
public class Order {
    @Emebedded
    private Orderer orderer;
}

Orderer.java

@Embeddable
public class Orderer {
   @Embedded
   @AttributeOverrides(
       @AttributeOverride(name = “id”, column = @Column(name = “orderer_id”))
   )
   private MemberId memberId;
   
   @Column(name = “orderer_name”)
   private String name;
}

Orderer 는 맴버 애그리거트를 ID로 참조한다. (여기서 맴버 아이디를 따로 클래스로 사용하고 있음, 그리고 Order 테이블에 맴버 아이디를 orderer_id로 사용하기 위해 애트리뷰트오버라이드를 사용하고 있음)

MemberId.java

@Embeddable
public class MemberId implements Serializable {
    @Column(name = “member_id”)
    private String id;
}
  • 엔티티와 밸류의 생성자는 객체를 생성할 때 필요한 것들을 받는데, 벨류 객체의 경우 불변 객체를 사용하는것이 좋음. 이때 JPA의 기술적 제약 때문에 기본 생성자가 필요한데 이것을 통해 만들면 불완전한 객체가 만들어짐. 그래서 기본생성자는 protected 로 해두는것이 좋음.

  • 엔티티를 객체가 제공할 기능 중심으로 구현하도록 할땐 JPA 매핑 처리를 프로터피가 아닌 필드로 선택해서 get/set 을 구현하지 않도록 한다.

@Entity
@Access(AccessType.FIELD)
public class Order {
}
  • 질문) @Access(AccessType.FIELD) ?

명시적으로 지정하지 않으면 @Id@EmbeddedId가 어디에 위치했는지에 따라 접근 방식을 결정한다. 접근 방식이라니??

  • AttributeConverter -> 재미가 없어서 패쓰

ID 참조와 조인 테이블을 이용한 단방향 M:N 매핑

  • 애그리거트간 집합 연관은 성능상 후지기 때문에 피해야함. 그래도 하고싶다면 단방향 집합 연관을 적용
@Entity
@Table(name = “product”)
public class Product{ 
    @EmbeddedId
    private ProductId id;
    
    @ElementCollection
    @CollectionTable(name = “product_category”, joinCloumns = @JoinColumn(name = “product_id”))
    private Set<CategoryId> categoryIds;

Product에서 Category로의 단방향 M:N 연관을 ID 참조 방식으로 구현. ElementCollectiopn을 사용해서 Product 삭제할때 조인테이블의 데이터도 함께 삭제됨. 애그리거트를 직접 참조하면 영속성 전파나 로딩 전략을 고민해야 하는데 ID 참조로 하면 고민이 필요 없음

애그리거트 로딩 전략

  • JPA 매핑 설정할때 중요한것은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것.
// product 는 완전한 하나여야 한다
Product product = productRepository.findById(id);
  • 이렇게 조회 시점에 완전한 상태가 되도록 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 FetchType.EAGER로 설정 -> 하지만 컬렉션에 대해서 이렇게 할 경우 문제가 됨. -> 조회 전용 쿼리를 작성하는것도 좋음 -> 하지만 항상 모든 값을 조회하거나 수정하는것이 아니라서 지연 로딩 전략도 나쁘지 않음

애그리거트의 영속성 전파

식별자 생성 기능

  • 사용자가 직접 생성
  • 도메인 로직으로 생성
  • DB를 이용한 일련번호 사용

Chapter 5

  • 리포지터리는 애그리거트 저장소. 저장, 조회, 삭제.

  • 조회시 검색 조건이 다양해지면 find 매서드를 정의할 수 없는데 이땐 스팩을 사용하면 됨 -> Specification<T> 단순히 풀 스캔을 통해서 필터링. -> 겁나 느리기 때문에 JPA에선 where 절을 통해 구현할 수 있음. -> CreterialBuilder, Predicate

  • entityManager.createQuery 를 통해 쿼리를 만들어도 굳.


Chapter 6

  • 반드시 서비스 레이어가 필요하다는 생각은 버리자. 서비스가 단순히 리포지터리 매서드를 통해 조회하는것 뿐이면 그냥 컨트롤러에서 리포지터리를 호출하는것도 괜찮다. -> 이것은 조회전용 기능과 CQRS 와 연관이 있음.

  • 비즈니스 로직은 도메인에 넣자

  • 값 벨리데이션 처리는 되도록이면 서비스 단에서. 하지만 스프링의 BindingResult 같은걸 쓸때는 컨트롤러에선 유효 값 검증(필수 값, 값 형식, 범위)을, 서비스단에선 논리적인 값(존재 유무, 일치 여부) 검증을 하자 -> 벨리데이터를 따로 뽑아내서 두군데서 사용하는것도 괜찮음.

  • 서비스의 파라미터, 리턴 타입은 표현영역에 의존하지 않아야 한다. -> 서비스에서 HttpRequest 등을 쓰지 말고 JoinRequest 같이 의존하지 않는 타입을 만들어 사용한다. -> 이는 테스트 작성을 쉽게 함.

  • 서비스의 인터페이스는 그닥 필요가 없어 보임 -> 인터페이스는 런타임에 어떤 기능/동작을 바꾸기 위함인데 보통 서비스는 하나임.

  • 구분되는 기능별로 서비스 클래스를 구현하는 방법도 좋다.

  • 서비스는 표현 계층 - 도메인 계층을 연결하는 파사드 역할. -> 단순한 역할만 함. 트랜잭션 처리 같은것만.

  • 질문) 서비스에서 여러 루트 애그리거트를 참조해서 작업할 땐? 이땐 도메인 로직이 서비스에 담기는거 같은데? -> 도메인 범위를 잘못 나눴거나


chapter 7 도메인 서비스

이전장에서 응용 서비스 계층과 표현 계층을 이야기 했는데 여기서 응용 서비스 계층과 도메인 서비스는 다른 계층임. 그리고 하는일도 다름.

응용 서비스 계층은 표현영역-도메인 레포지터리 영역을 연결해주는 퍼사드 개념이 크고(서로 연결해주고 트랜젝션 처리 등등) 도메인 서비스 계층은 말 그대로 도메인에 관련되서 여러 도메인에 대해 비즈니스 로직을 수행할 때 사용 할 수 있다. 특히 책에서는 여러 애그리거트가 필요한 기능을 의미하고 있는데, 하나의 애그리거트에 모두 우겨넣기 보단 여러 애그리거트가 필요한 경우에 따라서 따로 서비스객체로 만드는것이 좋은 전략인거 같다. 그리고 이녀석은 도메인 패키지에 위치하게 된다.

도메인 서비스는 도메인과 다르게 상태 없이 오직 로직만 구현한다. 그리고 필요한 도메인은 파라미터로 주입 받도록 한다. 그리고 응용 서비스에서 도메인에 도메인 서비스 객체를 넘겨주어 도메인으로 하여금 실행을 하도록 만든다.


chapter 8 애그리거트 트랜젝션 관리

한 애그리거트를 두개 이상의 쓰레드에서 수정, 커밋을 할 때 문제가 되는데 보통 락을 사용해서 관리를 함.

선점형(비관적) 락

디비로부터 로우를 조회할 때 해당 로우를 락을 건다. 그리고 다른 쓰레드에서 조회시 블락이 되도록 만든다. 이것의 문제는 교착상태가 발생 할 수 있고 나중에 커밋되는 데이터 때문에 잘못 될 수 있다.

비선점형(낙관적) 락

커밋할 때 커밋이 가능한지 확인을 하도록 한다. 버전 컬럼을 하나 더 둬서 업데이트를 할 때 버전을 1 증가 시킨다. 현재 애그리거트와 테이블의 버전이 같을때만 커밋이 가능하도록 한다. (커밋할때 현재 버전에 대해 커밋을 하는데 이미 다른곳에서 커밋을 해서 버전이 증가되었다면 이 커밋은 실패하게(OptimisticLockingFailureException) 만든다)

구현은 그냥 애그리거트에

@Version
private Long version;

이렇게 필드를 하나 추가하고 업데이트를 할때 버전을 체크하는 매소드를 통해 버전이 일치할때만 업데이트를 하도록 구현한다.

if (!order.matchVersion(req.getVersion())) {
  throw new OptimisticLockingException();
}

강제로 버전을 증가시킬 수 도 있음-> 특히 에그리거트 루트 안의 다른 엔티티가 변경되었을땐 루트에그리거트가 변경된것이 아니라서 @Version 으로 지정한 필드가 자동으로 업데이트 되지 않는데 이때 사용하면 됨.

return entityManager.find(Order.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT);

오프라인 선점 잠금

재미가 없어서 패쓰


Chapter 9 도메인 모델과 Bounded Context

도메인 모델과 경계

  • 도메인을 완벽하게 표현하는 단일 모델은 함정

한 도메인은 다시 여러 하위 도메인으로 구분되기 때문에 한 개의 모델로 여러 하위 도메인을 표현하려고 하면 모든 하위 도메인에 맞지 않는 모델을 만들게 됨.

  • 하위 도메인에 따라 다른 용어를 사용하는 경우(회원, 주문자, 보내는 사람) 처럼 나눠질 경우 다른 도메인으로 생각하면 쉬움

  • 이렇게 하위 도메인마다 사용하는 용어가 다름 -> 하위 도메인마다 모델을 만들어야함 -> 각긱 명시적으로 구분되는 경계를 가지고 섞이지 않도록 해야함

  • 모델은 특정한 컨텍스트(문맥)하에서 완전한 의미를 갖는다. -> 이렇게 구분되는 경계를 갖는 컨텍스트를 Bounded Context 라 함

같은 제품이라도 카탈로그 컨텍스트와 재고 컨텍스트에서 의미가 서로 다르다

Bounded Context

  • 모델의 경계를 설정하며 한개의 Bounded Context는 논리적으로 하나의 모델을 갖는다.

  • Bounded Context는 용어를 기준으로 구분한다.

카탈로그 컨텍스트와 재고 컨텍스트는 서로 다른 용어를 사용하고 있으므로 이 용어를 기준으로 컨텍스트를 분리

  • 물리적인 Bounded Context 가 한개이더라도 내부적으로 패키지를 활용해서 논리적으로 Bounded Context를 만든다.

같은 사용자라고 해도 주문 Bounded Context회원 Bounded Context가 갖는 모델이 달라짐.

Bounded Context의 구현

  • Bounded Context는 도메인 모델 뿐만 아리나 도메인 기능을 사용자에게 제공하는데 필요한 표현 영역, 응용 서비스, 인프라 영역까지 모두 포함

Bounded Context 간 통합

  • 예를들어 Rest API 를 통해 기능을 제공한다면 데이터가 제공하는 시스템의 모델을 기반으로 되어 있기 때문에 제공받는 시스템에선 그에 맞는 모델로 변환을 해야함 -> 좀 커지게 되면 따로 Translator로 만들고 위임

  • Rest Api처럼 직접 통합이 아니라 간접적으로 메시지 큐를 사용할 수 도 있음 -> 메시지 큐에서 주고받은 데이터 형식에 대해 협의해야함 -> 큐를 누가 제공하느냐에 따라 데이터 구조가 결정됨 -> pub/sub

  • 마이크로서비스와 Bounded Context

둘이 서로 잘 어울리는데 Bounded Context는 모델의 경계를 형성하는데 Bounded Context를 마이크로서비스로 구현하면 자연스럽게 컨텍스트별로 모델이 분리 -> 코드레벨에선 마이크로서비스마다 프로젝트를 생성하므로 Bounded Context마다 프로젝트를 만들게 됨 -> Bounded Context의 모델이 섞이지 않도록 해줌

Bounded Context간의 관계

Chapter 10 이벤트

  • 시스템간 강결합을 느슨하게 하기 위해서 또는 도메인 로직이 섞일 때, 다른 시스템과 연동을 해야할 때 유용하게 사용할 수 있다.

  • 이벤트: 과거에 벌어진 어떤 것 (사건?) -> 과거형임

  • 이벤트가 발생했다 -> 상태가 변경되었다 -> 이에 반응해서 원하는 동작 수행

이벤트 구성요소

[ 이벤트 생성 주체 ] --(이벤트)--> [ 이벤트 디스패처(퍼블리셔) ] --(이벤트)--> [ 이벤트 핸들러(서브스크라이버) ]

  • 이벤트 주체: 엔티티 / 벨류 / 도메인 서비스 같은 도메인 객체 -> 상태가 바뀌면 이벤트 발생

  • 이벤트 핸들러: 이벤트에 반응. 이벤트에 담긴데이터를 이용해서 원하는 기능 실행

  • 저 둘을 이어주는게 디스패처. 구현방식에 따라 생성/처리를 동기 또는 비동기로 실행

이벤트 구성

  • 이벤트 종류: 클래스 이름으로 이벤트 종류 표현
  • 이벤트 발생 시간
  • 추가 데이터: 주문번호같은 이벤트와 관련된 정보

ex)

public class ShippingInfoChangedEvent { // 과거형으로 이름 짓는다
    private String orderNumber;
    private long timestamp;
    private ShippingInfo newShippingInfo;
}
public class Order {
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
        Event.raise(new ShippingInfoChangedEvent(number, newShippingInfo));
    }
}
  • Event.raise는 디스패처를 통해 이벤트를 전파시키는 기능

  • 이벤트 핸들러

public class ShippingInfoChangedHandler implements EventHandler<ShippingInfoChangedEvent> {
    @Override
    public void handle(ShippingInfoChangedEvent event) {
        shippingInfoSync.sync(event.getOrderNumber(), event.getNewShippingInfo());
    }
}

이벤트 용도

  1. 트리거: 도메인의 상태가 바뀔 때 다른 후처리를 할 경우 후처리를 실행하기 위한 트리거로 사용
  2. 서로 다른 시스템간의 데이터 동기화

장점

  • 도메인 로직이 섞이는것을 방지

  • 이벤트 핸들러를 사용하면 기능 확장도 용이(OCP)

이벤트, 핸들러, 디스패처 구현

  • 디스패처에서 핸들러 등록 리스트를 관리하기 위해 ThreadLocal을 사용했음.

  • 이벤트 클래스

  • 이벤트 핸들러: 이벤트 핸들러를 위한 상위 타입으로 모든 핸들러는 이 인터페이스를 구현

  • 이벤트 디스패처(Events): 이벤트 발행, 핸들러 등록

이벤트 클래스

  • 필요한 데이터와 타임 스탬프 같은 값들이 있으면 됨

EventHandler 인터페이스

public interface EventHandler<T> {
    void handle(T event);
    
    default boolean canHandle(Object event) {
        Class<?>[] typeArgs = TypeResolver.resolveRawArguments(EventHandler.class, this.getClass());
        return typeArgs[0].isAssignableFrom(event.getClass());
    }
}

모든 핸들러를 돌면서 canHandle == true 일 때만 해당 핸들러를 실행시키는 형태 TypeResolver

Events 이벤트 디스패처

public class CancelOrderService {
    private OrderRepo orderRepo;
    private RefundService refundService;
    
    @Transactional
    public void cancel(OrderNo orderNo) {
        Events.handle(e->refundService.refund(e.getOrderNumber());
        
        Order order = findOrder(orderNo);
        order.cancel();
        
        Events.reset();
    }
}

이벤트가 발생하면 Events.handle에 전달한 핸들러로 이벤트 처리. 핸들러 목록 유지를 위해 ThreadLocal을 사용해서 이를 해제하기 위해 매서드 마지막 부분에 Events.reset() 호출 -> 매번 호출해줘야 해서 다음장에서 AOP로 수정함 동기적인 이벤트 처리 모델 -> 같은 트렌젝션 범위에 있음 이벤트 핸들러에서 이벤트를 발행한다면 무한 재귀에 빠질 수 있기 때문에 조심해야 함. -> 동시에 발행되는 이벤트를 제한

public class Events {
    // 생략...
}
  • 위에서 구현한 모덴을 동기 이벤트 처리 -> 이벤트 처리가 오래 걸린다면 문제가 됨 -> 일부 롤백? 전체 롤백?

비동기 이벤트 처리

  • 로컬 핸들러를 비동기로 실행하기
  • 메시지 큐
  • 이벤트 저장소와 이벤트 포워더 사용하기
  • 이벤트 저장소와 이벤트 제공 API 사용하기
로컬 핸들러의 비동기 실행
  • 위에서 구현한 녀석에서 ExecutorService를 활용해서 비동기로 처리
  • 비동기로 처리하게 되서 서로 다른 쓰레드 -> 서로 다른 트렌젝션
메시징 서비스를 이용한 비동기 구현
  • RabbitMQ 같은 메시징 큐를 사용하게 되면 비동기 처리가 가능한데 트랜젝션이 나눠짐(이벤트를 발행, 이벤트를 구독) -> 글로벌 트랜젝션을 사용하면 되지만 성능이 좋지 않음...
  • 메시지를 전달하기 위해 Kafka를 사용하기도 함
이벤트 저장소를 이용한 비동기 처리
  • 이벤트를 DB에 저장한 뒤, 별도의 프로그램으로 이벤트 핸들러에 전달하는데 크게 두가지 방법이 있음
  1. 포워더: 이벤트를 주기적으로 읽어와 전달. 어디까지 전달했는지를 추적
  2. API: REST와 같은 방식으로 이벤트를 외부에 제공 -> 이벤트 핸들러가 API를 통해 이벤트를 읽어오고 어디까지 읽었는지 추적해야함

이벤트 적용 시 추가 고려사항

  • 이벤트 소스(이벤트 발생 주체)를 EventEntry(DB에 저장되는 클래스)에 추가할지? -> 5가지 정도를 추가해야함

ex) Order가 발생한 이벤트만 조회하기 처럼 특정 주체가 발생한 이벤트만 조회하기.

  1. Events.raise()source를 파라미터로 추가
  2. EventHandler.handle()매서드에 source를 파라미터로 추가
  3. EventEntrysource 필드 추가
  4. EventStore.save()source 파라미터 추가
  5. EventStore.get()에 필터 조건으로 source파라미터 추가
  • 두번째로 고려할 점은 포워더에서 전송 실패를 얼마나 허용할 것인지

  • 이벤트 손실

  • 이벤트 순서

  • 이벤트 재처리 -> 기억하거나 처리를 멱등성으로


Chapter 11 CQRS

단일 모델의 단점

  • 조회시 여러 애그리거트에서 읽어올 경우가 많은데 이것을 JPA만으로 하게 되면 성능상 좋지 않을 수 있음 -> 로딩 방식에 따라서 달라짐 -> 이럴경우 조회 전용 모델을 만들어서 분리

  • Command Query Responsibility Segregation; 명령과 조회의 책임을 분리하기

  • 기존 애그리거트는 명령(Command)로 사용 -> 엔티티의 상태를 변경하는 명령

  • 조회 전용(Query) 모델을 생성 -> 상태를 조회만 함

  • 조회 전용 모델의 경우 중간에 응용 서비스 레이어가 없어도 됨 -> 컨트롤러가 직접 DAO에 접그냏서 데이터를 가져와도 무방 -> 조회 전용 모델의 인프라로 MyBatis 같은걸 사용해도 좋음; SQL을 주로 사용할 경우 / 또는 그냥 JPQL을 이용해도 좋음

  • 예제에선 Order: 상태 변경을 위한 도메인 모델, OrderSummary: 주문 목록 조회를 위한 모델

  • 명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수 도 있음 -> 성능 향상을 위해 조ghlsms NoSql 을 사용하고 두 저장소를 동기화할 수 도 있음

장점

  • 명령 모델을 구현할때 도메인 자체에 집중 할 수 있음 -> 복잡한 도메인은 주로 상태 변경 로직이 복잡함 -> 조회 관련 로직이 사라져 복잡도를 낮춤

  • 조회 성능을 향상시키는데 유리함 -> 조회에 특화된 쿼리를 마음대로 사용 가능 -> 캐시 뿐만 아니라, 조회 전용 저장소를 사용할 수 도 있음. 조회 전용 모델을 사용하기 때문에 조회 성능을 높이기 위한 코드가 명령 모델에 영향을 주지 않음

단점

  • 구현할 코드, 기술이 많음. (작은 서비스에선 오히려 배보다 배꼽이 더 클 수 있음)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment