[도메인 주도 개발 시작하기 - 최범균 저] 한 장 요약

2023. 1. 13. 02:50북리뷰/한 장 요약

728x90

도메일 모델 시작하기

  • 도메인에 따라 용어의 의미가 결정
    • 여러 하위 도메인을 하나의 다이어그램에 모델링 하면 안 됨
  • 각 하위 도메인마다 별도 모델을 만들어야

아키텍처 구성

  • 표현 영역 : 사용자 요청 처리/사용자에게 정보 제공
    • HTTP 요청을 응용영역이 필요로 하는 형식으로 변환헤서 전달
  • 응용 영역 : 사용자가 요청한 기능 실행 : 업무 로직을 직접 구현하지 않음
    • 응용 서비스를 로직을 직접 수행하지 않고, 도메인 모델에 로직 수행을 위임
  • 도메인 영역 : 시스템이 제공할 도메인 규칙 구현
    • 도메인 핵심 로직 구현
  • 인프라스트럭처 영역 : DB나 메시징 시스템 같은 외부 시스템 연동 처리

  • 상위 계층에서 하위 계층으로의 의존만 존재

  • DIP 적용시 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출

도메인 영역의 구성 요소

  • 엔티티 : 식별자를 가지며 객체 자신의 라이플 사이클 가짐
    • 데이터와 함꼐 도메인 기능을 함께 제공
  • 벨류 : 식별자 없음, 하나의 값을 표현할 때 사용
    • 불변으로 구현해야
    • 변경시 기존 객체값을 변경하지 않고, 새로운 객체를 할당

  • 에그리거트 : 엔티티와 벨류 객체를 개념적으로 묶은 것
    • 도메인 모델에서 전체 구조를 이해
    • 군집에 속한 객체를 관리하는 루트 엔티티를 가짐
    • 루트 엔티티를 통해 간접적으로 애그리거트 내 다른 엔티티나 벨류 객체에 접근

  • 리포지터리 : 도메인 모델의 영록성 처리 : DB 에 저장 및 불러옴
  • 도메인 서비스 : 특정 엔티티에 속하지 않은 도메인 로직 제공
    • ex) 상품 쿠폰 할인 금액 계산
  • 한 패키지에 가능하면 10~15개 미만 타임 개수 유지
  • 한 에그리커트에 속한 객체는 다른 에그리거트에 속하지 않음
  • "A가 B를 갖는다" 라고 해석할 수 있는 요구사항이 있다 하더라도 "A와 B가 한 애그리거트에 속한다는 것"을 의미하는 것은 아님
  • 최대한 한 애그리거트 당 한 개의 엔티티만
  • 트랜젝션 범위는 작을수록 좋다
  • 한 트랙젝션에서는 한 개의 애그리거트만 수정
  • 응용 서비스에서 두 애그리거트를 수정하도록 구현
  • 리포지터리는 애그리거트 단위로 존재
    • Order 와 OrderLine을 위한 리포지터리를 각각 만들지 않음
  • 애그리거트 루트와 매핑되는 테이블 뿐 아니라 애그리거트에 속한 모든 구성요소에 매핑된 테이블 데이터를 저장해야
  • order 애그리거트는 orderLine, Orderer 등 모든 구성 요소를 포함해야
  • 애그리거트 상태가 변경시 모든 변경을 원자적으로 저장소에 반영
  • 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야
  • 다른 애그리거트 잠조시 ID만을 이용해서 참조

  • 도메인 모델에 Getter/Setter메서드를 무조건 추가하는것 좋지 않음

 

리포지터리와 모델 구현

  • 가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치
  • JPA 매핑 처리를 프로터티 방식이 아닌 필드방식으로 선택하여 불필요한 getter/setter 구현 방지
  • 매핑 설정과 다른 이름 컬럼 사용시 @AttributeOverride 에너테이션 사용
@Embedded
@AttributeOverrides({
		@AttributeOverride(name = zipCode, 
                              column = @Column(name = shipping_zipcode)), 
		@AttributeOverride(name = address1, 
                              column = @Column(name = shipping_addr1)),
		@AttributeOverride(name = address2, 
                              column = @Column(name = shipping_addr2))
})
private Address address;

 

  • 루트 엔티티는 이하 벨류 타입 프로퍼티에 @Embedded 사용
@Entity
public class Order { 
   ...   
 @Embedded
 private Orderer orderer;
 @Embedded
 private ShippingInfo shippingInfo; 
 ...
}

 

  • AttributeConverter을 이용해서 밸류 타입과 컬럼 데이터 간 변환 처리
@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<Money, Integer> {
    @Override    
    public Integer convertToDatabaseColumn(Money money) {
        return money = = null ? null : money.getValue(); 
   }   
    @Override
    public Money convertToEntityAttribute(Integer value) { 
       return value = = null ? null : new Money(value);
    }
}

@Entity
@Table(name = purchase_order)
public class Order {    
    ....    
    @Column(name = total_amounts)
    //@Convert(converter = MoneyConverter.class) // 만약 Converter의 autoApply가 false일 경우 해당 애너테이션 추가
    private Money totalAmounts; // MoneyConverter를 적용해서 값 변환

.....
  • 벨류 컬랙션을 별도 테이블로 매핑시 @ElelmentCollection과 @CollectionTable을 함께 사용
@Entity@Table(name = purchase_order)
public class Order {
    @EmbeddedId
    private OrderNo number;
    ...
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = order_line,
                     joinColumns = @JoinColumn(name = order_number))
    @OrderColumn(name = line_idx)
    private List<OrderLine> orderLines;
    ...
  • @OrderColumn 애너테이션을 이용해서 지정한 칼럼에 리스트의 인덱스 값 저장
  • @CollectionTable은 벨류를 저장할 테이블을 지정
  • 벨류 타입을 식별자로 매핑시 @Id 대신 @EmbeddedId 사용
@Entity
@Table(name = purchase_order)
public class Order {
    @EmbeddedId
    private OrderNo number;
    ...
}
@Embeddable
public class OrderNo implements Serializable {
    @Column(name=order_number)
    private String number;
    ...
}
  • 식별자로 사용한 벨류 타입은 Serializable 인터페이스 상속
  • 벨류를 매핑한 테이블(ex: ArticleContent )을 지정하기 위해 @SecondaryTable과 @AttributeOverride 사용
    • 지연 로딩 방식으로 설정할 수 있으나, 벨류 타입을 엔티티로 만드는 것이므로 좋지 않음
@Entity
@Table(name = article)
@SecondaryTable(
        name = article_content,
        pkJoinColumns = @PrimaryKeyJoinColumn(name = id))
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    @AttributeOverrides({
      @AttributeOverride(
        name = content,
        column = @Column(table = article_content, name = content)),
      @AttributeOverride(
        name = contentType,
        column = @Column(table = article_content, name = content_type))
    })
    @Embedded
    private ArticleContent content;

    ...
  • @Embeddable 타입 클래스는 상속 매칭 지원 X
  • 상속 구조를 갖는 밸류 타입 사용시는 @Entity 사용
    • @Inheritance 적용
    • @DiscriminatorColumn 이용하여 타입 구분용으로 사용할 칼럼 지정
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = image_type)
@Table(name = image)public abstract class Image {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = image_id)
    private Long id;
    @Column(name = image_path)
    private String path;
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = upload_time)
    private Date uploadTime;
    protected Image() {}
    public Image(String path) {
        this.path = path;
        this.uploadTime = new Date();
    }
    protected String getPath() {
        return path;
    }
    public Date getUploadTime() {
        return uploadTime;
    }
    public abstract String getURL();
    public abstract boolean hasThumbnail();
    public abstract String getThumbnailURL();
}

@Entity
@DiscriminatorValue(II)
public class InternalImage extends Image {
    ...
}
@Entity
@DiscriminatorValue(EI)
public class ExternalImage extends Image {
    ...
}

@Entity
@Table(name = product)
public class Product {
    @EmbeddedId
    private ProductId id;
    private String name;
    @Convert(converter = MoneyConverter.class)
    private Money price;
    private String detail;
    @OneToMany(
            cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
            orphanRemoval = true)
    @JoinColumn(name = product_id)
    @OrderColumn(name = list_idx)
    private List<Image> images = new ArrayList<>();
    ...
    public void changeImages(List<Image> newImages) {
        images.clear();
        images.addAll(newImages);
    }
}
  • @OneToMany 매핑에서 컬렉션 clear() 매서드 실행시 각 엔티티에 대해 delete 쿼리 실행, 임베디드 타입 컬렉션은 한 방에 삭제 처리 수행

애그리거트의 영속성 전파

  • 저장/삭제 메서드는 애그리거트 루트만 저장/삭제하면 안됨, 이그리거트 소속 모든 객체를 저장/삭제해야
  • cascade 속성 적용

스프링 데이터 JPA를 이용한 조회 기능

  • 모든 DB 연동 코드를 JPA만 사용해서 구현해야 한다고 생각 X
  • 스팩 : 검색 조건을 다양하게 조합할 때 사용
public class OrdererIdSpec implements Specification<OrderSummary> {
    private String ordererId;
    public OrdererIdSpec(String ordererId) {
        this.ordererId = ordererId;
    }
    @Override
    public Predicate toPredicate(Root<OrderSummary> root,
                                 CriteriaQuery<?> query,
                                 CriteriaBuilder cb) {
        return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
    }
}


//스펙 생성 여러개
public class OrderSummarySpecs {
    public static Specification<OrderSummary> ordererId(String ordererId) {
        return (Root<OrderSummary> root, CriteriaQuery<?> query,
               CriteriaBuilder cb) ->
                cb.equal(root.<String>get("ordererId"), ordererId);
    }
    public static Specification<OrderSummary> orderDateBetween(
            LocalDateTime from, LocalDateTime to) {
        return (Root<OrderSummary> root, CriteriaQuery<?> query,
               CriteriaBuilder cb) ->
                cb.between(root.get(OrderSummary_.orderDate), from, to);
    }
}
  • 정적 메타 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드 정의
@StaticMetamodel(OrderSummary.class)public class OrderSummary_ {
    public static volatile SingularAttribute<OrderSummary, String> number;
    public static volatile SingularAttribute<OrderSummary, Long> version;
    public static volatile SingularAttribute<OrderSummary, String> ordererId;
    public static volatile SingularAttribute<OrderSummary, String> ordererName;
     //생략
}
  • 정렬 순서를 지정할 때는 Sort 사용
  • 페이징 관련 기능은 Page/Pageable 사용
  • 페이징 처리와 관련된 정보가 필요 없으면 Page 리턴 타입이 아닌 List를 사용하여 불필요한 Count 쿼리 실행 않도록 함
  • @Subselect : @Entity로 매핑하여 쿼리 실행 결과를 매핑할 테이블 처럼 사용
    • 표현 영역을 통해 사용자에게 데이터를 보여주기 위함
    • 수정 불가, 만약 변경하더라고 DB 반영 X
@Entity
@Immutable@Subselect(
        """
        select o.order_number as number,
        o.version, o.orderer_id, o.orderer_name,
        o.total_amounts, o.receiver_name, o.state, o.order_date,
        p.product_id, p.name as product_name
        from purchase_order o inner join order_line ol
            on o.order_number = ol.order_number
            cross join product p
        where
        ol.line_idx = 0
        and ol.product_id = p.product_id""")
@Synchronize({"purchase_order", "order_line", "product"})
public class OrderSummary { 
   @Id 
   private String number;
    private long version;
    @Column(name = "orderer_id")
    private String ordererId;
    @Column(name = "orderer_name")
    private String ordererName;
    //생략
    protected OrderSummary() {
    }
  • @Synchronize를 통해 앤티티 로딩 전 지정한 테이블과 관련한 변경 발생시 플러시를 먼저 함

응용 서비스와 표현 영역

  • 표현영역: 응용 서비스가 요구하는 형식으로 사용자 요청을 반환
  • 응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성 있음.
  • 응용 서비스는 트랜젝션 처리도 담당
  • 공통 로직을 별도 클래스로 구현하여 코드 중복을 방지
  • 응용 서비스를 굳이 인터페이스가 필요하다고 생각 안함
    • 런타임 교체 경우가 거의 없다
    • 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다
  • 에그리거트 자체를 리턴하면 편하다, 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있음
    • 아무데서나 다 수정하기 떄문에 안좋아... 편하면 다 문제가 있다니까
  • 표현 영역에 해당하는 HttpSession/HttpServletRequest 그대로 응용 서비스 파라미터에 전달 X

표현 영역

  • 화면 흐름 제공/제어
  • 요청에 알맞은 응용 서비스에 전달/ 결과를 사용자에게 제공
  • 세션 관리
  • 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있음.
    • 원칙적으로는 응용 서비스에서 처리
  • 표현 영역에서는 필수 값, 값 형식, 범위 검증
  • 응용서비스에서는 데이터 존재 유무 같은 논리적 오류 검증
  • 표현 영역에서 인가 검사
  • 응용 서비스가 사용자 요청 기능을 실행하는데 별 다른 기여를 하지 못하면 굳이 서비스 안만들어도 됨

도메인 서비스

  • 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안 됨
    • 자신의 책임 범위를 넘어서는 기능을 구현하기 때문
  • 도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용
    • ex)
      • 계산 로직
      • 외부 시스템 연동이 필요한 도메인 로직
  • 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러냄
  • 도메인 서비스는 도매인 로직을 다룸
  • 도메인 서비스는 상태 없이 로직만 구현
  • 필요한 상태는 다른 방법으로 전달
  • 자주 사용하는 주체는 애그리거트가 될 수 있고 응용 서비스가 될 수도 있음
  • 도메인 서비스 객체를 애그리거트에 주입 (비추)
    • 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임
    • 좋은 방법이 아님
    • 일부 기능을 위해 굳이 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없음
  • 도메인 서비스는 응용 로직을 수행하지 않음
  • 외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있음
  • 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스 분리
  • 도메인 서비스 구현이 특정 구현 기술에 의존하거나 외부 시스템 API를 실행한다면 도메인 서비스는 인터페이스로 추상화

에그리거트 트랜젝션 관리

  • 선점 잠금 : @Lock 에너테이션을 사용해서 잠금 모드 지정
    • 교착상태 발생하지 않도록 주의
    • 잠금 구할 떄 최대 대기 시간 지정
      • hint 또는 @QueryHints 사용
  • 비선점 잠금 : 애그리거트와 매핑되는 테이블 버전 값이 현재 애그리거트 버정과 동일시 수정
    • @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 컬럼 추가
    • 전달할 요청 데이터는 사용자가 전송한 버점 값을 포함해야
    • 일치하는 경우만 기능 수행
    • VersionConflictException : 누가 이미 수정함, OptimisticLockingFailureException : 거의 동시에 수정함
  • 오프라인 섬점 방식 : 여러 트랜잭션에 걸쳐 동시 변경 방지
    • 잠금 유효 시간 가져야
    • 필수 기능 :
      • 잠금 선점 시도
      • 잠금 확인
      • 잠금 해제
      • 잠금 유효기간 연장
    • LockId를 어딘가에 보관

도메인 모델과 바운디드 컨텍스트

  • 하위 도메인 마다 사용 용어가 달라서 하위 도메인 마다 각기 모델을 만들어야
  • 각 모델은 명시적으로 구분되는 경계를 가져 섞이지 않도록 해야
  • 바운디드 컨텍스트 : 구분되는 경계를 갖는 컨텍스트
  • 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 가짐
  • 바운디드 컨텍스트는 용어를 기준으로 구분
  • 이상적으로 하위 도메인과 바운디드 컨텍스트가 일대일 관계 : 현실적으로 어려움
  • 바운디드 컨텍스트는 표현/응용 서비스/ 인프라스트럭처 영역을 모두 포함
  • 모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요 X
  • 바운디드 컨텍스트가 반드시 사용자에게 보여주는 UI 필요 X
  • 바운디드 컨텍스트 간 통합시 두 바운디드 컨텍스트 간 사용할 메시지 데이터 구조를 맞춰야
  • 카탙로그 시스템에서 큐를 제공한다면 큐에 담기는 내용은 카탈로그 도메인을 따라야
  • 마이크로 서비스 특징과 잘 어울림
  • 공급자와 소비자 관계에 있는 두 팀은 상호 협력이 필수
  • 하류 컴포는트는 상류 서비스 모델이 자신에 도메인 모델에 영항 주지 않도록 보호해주는 완충 지대 필요

이벤트

  • 과거에 벌어진 어떤 것
  • 구성요소
    • 이벤트
    • 생성 주체
      • 엔티티/벨루/도메인 서비스 같은 도메인 객체
    • 디스페처(퍼블리셔)
      • 생성 주체와 핸들러를 연결
    • 핸들러(구독자)
      • 이벤트 생성 주체가 발생한 이벤트에 반응
  • 이벤트 구성
    • 이벤트 종류(클래스 이름)
    • 이벤트 발생 시간
    • 추가 데이터 : Ex) 주문번호 등, 이벤트 관련 정보
  • 이벤트는 데이터를 담아야 하나, 이벤트 자체와 관련 없는 데이터를 포함할 필요 X
  • 이벤트 용도
    • 트리거 : 후처리를 실행하기 위한 트리거
    • 서로 다른 시스템 간 데이터 동기화
  • 이벤트를 사용함으로서 서로 다른 도메인 로직이 섞이는 것을 방지
  • 과거에 벌어진 상태 변화나 사건을 의미 : 이벤트 클래스 이름은 과거 시제로
  • 이벤트 처리하는데 필요한 최소한의 데이터만 포함
  • 비동기 이벤트 처리 방법
    • 로컬 핸들러 비동기로 실행
    • 메시지큐 이용
    • 이벤트 저장소와 이벤트 포워더 사용
    • 이벤트 저장소와 이벤트 제공 API 사용
  • 메서드를 주기적으로 실행하고 싶다면 @Scheduled 사용
  • 추가 고려 사항
    • 이벤트 소스를 EventEntry에 추가할지 여부
    • 포워더에서 전송 실패를 얼마나 허용하냐
      • 별도 실패용 DB나 메시지큐에 저장하기도
    • 이벤트 손실에 대한 것
    • 이벤트 순서
    • 이벤트 재처리
      • 멱등성 처리 방법도

CQRS

  • 상태 변경 모델과 조회 모델을 분리
  • 조회 모델에 응용서비스 필요 없을수도
  • 상태 변경 모델은 도메인 모델 기반, 조회 모델은 데이터 타입만 이용
  • 명령 모델은 상태 변경하는 도메인 로직을 수행하는 데 초점
  • 조회 모델은 화면에 보여줄 데이터를 조회하는 데 초점

  • 두 데이터 저장소 간 데이터 동기환느 이벤트를 활용해서 처리
  • 웹서비스는 일반적으로 상태 변경 요청보다 조회 요청이 많음
  • 조회 성능을 향상하는데 유리
728x90