[도메인 주도 개발 시작하기 - 최범균 저] 한 장 요약
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)
- 계산 로직
- 외부 시스템 연동이 필요한 도메인 로직
- ex)
- 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러냄
- 도메인 서비스는 도매인 로직을 다룸
- 도메인 서비스는 상태 없이 로직만 구현
- 필요한 상태는 다른 방법으로 전달
- 자주 사용하는 주체는 애그리거트가 될 수 있고 응용 서비스가 될 수도 있음
- 도메인 서비스 객체를 애그리거트에 주입 (비추)
- 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임
- 좋은 방법이 아님
- 일부 기능을 위해 굳이 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없음
- 도메인 서비스는 응용 로직을 수행하지 않음
- 외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있음
- 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스 분리
- 도메인 서비스 구현이 특정 구현 기술에 의존하거나 외부 시스템 API를 실행한다면 도메인 서비스는 인터페이스로 추상화
에그리거트 트랜젝션 관리
- 선점 잠금 : @Lock 에너테이션을 사용해서 잠금 모드 지정
- 교착상태 발생하지 않도록 주의
- 잠금 구할 떄 최대 대기 시간 지정
- hint 또는 @QueryHints 사용
- 비선점 잠금 : 애그리거트와 매핑되는 테이블 버전 값이 현재 애그리거트 버정과 동일시 수정
- @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 컬럼 추가
- 전달할 요청 데이터는 사용자가 전송한 버점 값을 포함해야
- 일치하는 경우만 기능 수행
- VersionConflictException : 누가 이미 수정함, OptimisticLockingFailureException : 거의 동시에 수정함
- 오프라인 섬점 방식 : 여러 트랜잭션에 걸쳐 동시 변경 방지
- 잠금 유효 시간 가져야
- 필수 기능 :
- 잠금 선점 시도
- 잠금 확인
- 잠금 해제
- 잠금 유효기간 연장
- LockId를 어딘가에 보관
도메인 모델과 바운디드 컨텍스트
- 하위 도메인 마다 사용 용어가 달라서 하위 도메인 마다 각기 모델을 만들어야
- 각 모델은 명시적으로 구분되는 경계를 가져 섞이지 않도록 해야
- 바운디드 컨텍스트 : 구분되는 경계를 갖는 컨텍스트
- 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 가짐
- 바운디드 컨텍스트는 용어를 기준으로 구분
- 이상적으로 하위 도메인과 바운디드 컨텍스트가 일대일 관계 : 현실적으로 어려움
- 바운디드 컨텍스트는 표현/응용 서비스/ 인프라스트럭처 영역을 모두 포함
- 모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요 X
- 바운디드 컨텍스트가 반드시 사용자에게 보여주는 UI 필요 X
- 바운디드 컨텍스트 간 통합시 두 바운디드 컨텍스트 간 사용할 메시지 데이터 구조를 맞춰야
- 카탙로그 시스템에서 큐를 제공한다면 큐에 담기는 내용은 카탈로그 도메인을 따라야
- 마이크로 서비스 특징과 잘 어울림
- 공급자와 소비자 관계에 있는 두 팀은 상호 협력이 필수
- 하류 컴포는트는 상류 서비스 모델이 자신에 도메인 모델에 영항 주지 않도록 보호해주는 완충 지대 필요
이벤트
- 과거에 벌어진 어떤 것
- 구성요소
- 이벤트
- 생성 주체
- 엔티티/벨루/도메인 서비스 같은 도메인 객체
- 디스페처(퍼블리셔)
- 생성 주체와 핸들러를 연결
- 핸들러(구독자)
- 이벤트 생성 주체가 발생한 이벤트에 반응
- 이벤트 구성
- 이벤트 종류(클래스 이름)
- 이벤트 발생 시간
- 추가 데이터 : Ex) 주문번호 등, 이벤트 관련 정보
- 이벤트는 데이터를 담아야 하나, 이벤트 자체와 관련 없는 데이터를 포함할 필요 X
- 이벤트 용도
- 트리거 : 후처리를 실행하기 위한 트리거
- 서로 다른 시스템 간 데이터 동기화
- 이벤트를 사용함으로서 서로 다른 도메인 로직이 섞이는 것을 방지
- 과거에 벌어진 상태 변화나 사건을 의미 : 이벤트 클래스 이름은 과거 시제로
- 이벤트 처리하는데 필요한 최소한의 데이터만 포함
- 비동기 이벤트 처리 방법
- 로컬 핸들러 비동기로 실행
- 메시지큐 이용
- 이벤트 저장소와 이벤트 포워더 사용
- 이벤트 저장소와 이벤트 제공 API 사용
- 메서드를 주기적으로 실행하고 싶다면 @Scheduled 사용
- 추가 고려 사항
- 이벤트 소스를 EventEntry에 추가할지 여부
- 포워더에서 전송 실패를 얼마나 허용하냐
- 별도 실패용 DB나 메시지큐에 저장하기도
- 이벤트 손실에 대한 것
- 이벤트 순서
- 이벤트 재처리
- 멱등성 처리 방법도
CQRS
- 상태 변경 모델과 조회 모델을 분리
- 조회 모델에 응용서비스 필요 없을수도
- 상태 변경 모델은 도메인 모델 기반, 조회 모델은 데이터 타입만 이용
- 명령 모델은 상태 변경하는 도메인 로직을 수행하는 데 초점
- 조회 모델은 화면에 보여줄 데이터를 조회하는 데 초점
- 두 데이터 저장소 간 데이터 동기환느 이벤트를 활용해서 처리
- 웹서비스는 일반적으로 상태 변경 요청보다 조회 요청이 많음
- 조회 성능을 향상하는데 유리
728x90
'북리뷰 > 한 장 요약' 카테고리의 다른 글
[아파치 카프카 애플리케이션 프로그래밍 with 자바 - 최원영 저] 한 장 요약 (0) | 2023.03.17 |
---|---|
[자바 코딩, 이럴 땐 이렇게 - 배병선 저] 한 장 요약 (0) | 2022.12.27 |
[한 권으로 읽는 컴퓨터 구조와 프로그래밍 - 조너선 스타인하트 저] 한 장 요약 (1) | 2022.12.25 |
[함께 자라기 - 김창준 저] 한 장 요약 (0) | 2022.12.06 |
[Unit testing(단위 테스트)-블라디미르 코리코프 저] 한 장 요약 (2) | 2022.12.04 |