kok202
DDD Start! (1/2)

2021. 10. 2. 02:13[공부] 독서/DDD Start!

http://www.yes24.com/Product/Goods/27750871

 

DDD START! - YES24

DDD의 핵심 개념을 배우고 구현으로 익히기!이 책은 DDD(도메인 주도 설계)를 처음 접하는 개발자를 위한 책이다. DDD를 실제 업무에 적용할 수 있도록 기본적인 이론을 설명하고 이를 구현한 코드

www.yes24.com

https://github.com/madvirus/ddd-start

 

GitHub - madvirus/ddd-start

Contribute to madvirus/ddd-start development by creating an account on GitHub.

github.com

 

01. 도메인 모델 시작

도메인 모델의 아키텍처 구성

- 표현 계층: Controller

- 응용 계층: Service

- 도메인 계층: Domain Entity + DB Entity + Repository

- 인프라 계층: Repository 구현체(ex. JPA, MyBatis) + 외부 시스템과의 연동 처리

개념 모델은 데이터베이스, 트랜잭션, 성능, 구현 기술을 고려하지 않은 것으로 실체화 하면 구현 모델이 된다. 개념 모델은 실제로 사용 가능한 모델은 아니다.

 

엔티티와 밸류의 차이

엔티티: 엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. 엔티티의 식별자는 변하지 않는다. 따라서 식별자가 같다면 같은 엔티티이다.

밸류 타입: 밸류 타입은 불변 객체인 것이 좋다. 불변 타입을 사용하면 안전한 코드를 작성할 수 있다.

 

도메인 모델에는 set 메소드를 넣지 말아야한다.

set 메소드로 데이터를 전달하도록 하면 생성하는 시점에는 도메인이 불완전하며, 완전한 도메인이라는 목표를 달성할 수 없을 수 있다.
습관적인 public set을 피해야한다. 일반적으로 public set은 그냥 데이터를 할당하는 것로 끝나는 정도이고 잘해야 null 체크 정도이다.
캡슐화를 방해하므로 조심해야한다.

Order order = new Order();
order.setOrderLines(lines);
order.setShippingInfo(shippingInfo); // 개발자의 실수로 어떤 코드에서는 누락이 될 수도 있다.

 


02. 아키텍쳐 개요

문맥으로 파악한 바에 의하면 도메인 계층은 인프라 계층에 직접 의존해선 안된다.

 

DIP 주의사항

저수준 모듈이 고수준 모듈을 이용해야하는 경우라면 DIP를 사용해야한다. DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함이다. 예를들어 OrderRepository와 JPAOrderRepository가 둘다 인프라 계층에 있고, 도메인 계층에서 OrderRepository를 의존하고 있다면 DIP가 잘못 적용되고 있는 것이다. OrderRepository는 도메인 계층에, JPAOrderRepository 는 인프라 계층에 있어야한다.

Entity 고유의 식별자를 갖는 객체, 라이프 사이클을 갖는다.
Value 고유의 식별자를 갖지 않는 객체, 개념적으로 하나인 도메인 객체의 속성을 표현한다.
Aggregate 관련된 Entity와 Value를 개념적으로 하나로 묶은 것. (ex. Order + OrderLine + Orderer는 '주문'이라는 Aggregate로 묶을 수 있다.)
Domain service 특정 엔티티에 속하지 않은 도메인 로직을 제공하는 서비스

 

* 도메인 엔티티와 관계형 DB의 엔티티는 같은 것이 아니다. 도메인 엔티티는 도메인 기능도 제공할 수 있다.

 


03. Aggregate

관련된 모델을 하나로 모은 것, 한 Aggregate 에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다. '주문'이라는 Aggregate를 만들기 위해선 Order + OrderLine + Orderer 같은 관련 객체도 함께 생성되어야한다. Aggregate 에는 보통 한개의 엔티티 객체만 존재한다.  두 개 이상이 존재하는 경우도 있지만 드문 케이스이므로 점검이 필요하다.

 

하나의 Aggregate가 관리하는 범위는 자기 자신으로 한정해야한다.

order.getOrderer().getMember().getId()

 

위와 같이 사용하면 다음과 같은 문제가 생긴다.

1. 편한 탐색 오용

2. 성능에 대한 고민

3. 확장 어려움

 

한 Aggregate에서 다른 Aggregate의 상태를 변경하는 것은 Aggregate 간의 의존 결합도를 높인다. 결과적으로 Aggregate 의 변경을 어렵게 만든다.

public class Order{
  public void changeShippingInfo(...){
    ...
    // 이러한 유혹에 빠지기 쉽다. (Order Aggregate 에서 Customer Aggregate에 있는 값을 변경하고 있다.)
    orderer.getCustomer().changeAddress(newShippingInfo.getAddress());
  }
}

이러한 문제는 연결 관계를 Id 를 이용한 간접 참조 방식을 이용하게 함으로써 완화시킬 수 있다.

public class ChangeOrderServce{
  public void changeShippingInfo(...){
    ...
    order.changeShippingInfo(...);
    Customer customer = customerRepository.findById(order.getOrderer().getCustomerId());
    customer.changeAddress(newShippingInfo.getAddress());
  }
}

단방향 M:N 연관만 적용하는 것이 좋다.

@Entity
@Table(name = "product")
public class Product{
  ...

  @ElementCollection
  @CollectionTable(
    name = "product_category",
    joinColumns = @JoinColumn(name = "product_id"))
  private List categoryIds;

  ...
}

Aggregate가 갖고 있는 데이터를 이용해서 다른 Aggregate을 생성해야 한다면 Aggreate 에 팩토리 메소드를 구현하는 것을 고려해보는 것이 좋다.

 


04. 리포지토리와 모델 구현(JPA 중심)

Repository Interface는 Aggregate와 같은 도메인 계층에 속한다. Repository 의 구현체는 인프라 계층에 속한다. 이렇게하여 인프라 계층에 대한 의존도를 낮춘다.Repository 구현의 기본 목적은 아래와 같다.

1. 아이디로 Aggregate 조회

2. Aggregate 저장

 

Aggregate를 표현하기

Aggregate 루트는 엔티티이므로 @Entity 로 매핑 설정한다. 한 테이블에 엔티티와 밸류 데이터가 같이 있다면, 밸류는 @Embeddable로 매핑 설정하고 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.

 

Aggregate와 루트 엔티티

Aggregate 에서 루트 엔티티를 제외한 나머지는 대부분 밸류이다. 루트 엔티티 외에 다른 엔티티가 있다면 진짜 엔티티인지 의심해봐야한다. 진짜 엔티티라는 생각이 든다면 다른 Aggregate은 아닌지 확인해야한다.

Aggregate는 완전해야한다.

1. 상태 변경하는 기능을 실행할 때, Aggregate 상태가 완전해야하기 때문이다.

2. 표현 계층에서 Aggregate 상태를 보여줄 때 필요하기 때문이다.

따라서 Repository에서 Aggregate을 로딩했을 때 완전한 하나가 로딩될 수 있어야한다.

// product Aggregate 은 완전한 상태일 수 있어야한다.
Product product = productRepository.findById(id);

 

* 기타: 카타시안 조인을 사용하면 쿼리 결과에 중복이 발생한다.

 


05. 리포지토리의 조회 기능(JPA 중심)

JPA를 사용하다보면 findBy 메소드가 계속해서 늘어나게되고 지나치게 많아지다보면 관리가 힘들어진다. 책에서는 JPA의 CriteriaBuilder와 Predicate을 이용한 Specification 클래스를 개발하는 방법에 대해 소개하고 있다. 구현이 대부분이라 적지 않는다.

 

QueryDSL을 사용하는게 나을지도...? 사실상 내부구현도 유사할 것으로 보인다.

 


JPA 어노테이션 

@Entity 엔티티를 매핑할 때 사용하는 어노테이션.
@Component 밸류 타입을 매핑할 때 사용하는 어노테이션. 연관관계를 표현하기 위한 어노테이션.
@Embeddable 다수의 프로퍼티를 하나의 클래스로 정의했을 때 클래스에 달아주는 어노테이션.
@Embedded @Embeddable 클래스를 사용할 때 달아주는 어노테이션. @AttribugteOverrides를 같이 사용하는 경우가 많다.
@CollectionTable 별도의 매핑 테이블을 이용해 객체 간의 연관관계를 표현할 때 사용할 수 있는 어노테이션. @ElementCollection과 같이 사용된다.
@SecondaryTable 밸류를 매핑한 테이블을 지정하기 위한 어노테이션. @AttributeOverride와 같이 사용된다.

 

JPA에서 @Entity 와 @Embeddable 로 클래스를 매핑하려면 기본 생성자가 필요한데, 기본 생성자는 가능하면 protected 로 지정한다. 지연 로딩 대상이 될 수 있기 때문에 private 이 아닌 protected 로 지정한다. 

 

다형성 관련 어노테이션 

@DiscribinatorColumn 객체의 type을 이용한 다형성을 표현할 때 사용할 수 잇는 어노테이션. @DiscribinatorValue와 같이 사용된다.
@Inheritance 객체의 다형성을 표현할 때 테이블을 관리하는 방법을 어떤 식으로 할지 전략을 택할 수 있다. 하나의 테이블에 저장할지 클래스별로 다른 테이블에 저장할지.

 

하이버네이트 어노테이션 

@Immutable 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영을 하지 않고 무시하게 하는 어노테이션.
@Subselect Subselect에 정의된 쿼리를 값으로 갖게하는 어노테이션.
@Synchronize 데이터를 로딩 전에 변경이 발생하면 먼저 플로시하도록 하는 어노테이션.

 

'[공부] 독서 > DDD Start!' 카테고리의 다른 글

DDD Start! (2/2)  (0) 2021.10.02