예전 부터 아껴놓다가 드디어 한 번 읽어보는 DDD Start!
DDD-START
도메인 모델시작하기
- Get/Set method 습관 X
- 의미 있는 단어로 사용
- 불변 객체 사용하기
- 생성자를 적극적으로
- 객체를 비지니스적으로 불완전한하게 시작하지 말자 만약 불완전하게 시작이 된다면 그때는 리팩토링을 생각해보자.
- 유비쿼터스 언어
- 전문가 관게자 개발자 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서 도메인모델, 코드 테스트 등 모든 곳에서 같은 용어를 사용한다. 이렇게 하면 소통의 모호함이 줄어들고 불필요한 해석 과정을 줄일 수 이싿.
- 도메인 용어에 알맞은 단어를 찾는 시간을 아까워 하지 말자.
인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다. RDMS 연동을 처리하거나 메시징 큐에 메세지를 전송하거나 수신하는 기능, 몽고 DB나 redis와의 데이터 연동을 처리한다. SMTP를 이용한 메세지 발송 기능을 구현하거나 http클라이언트를 이용해서 rest api를 호출하는 것도 처리한다. 실제 구현을 다룬다. 도메인 영역, 응용 영역, 표현 역역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
계층 구조
고수준 모듈
은 의미있는 단일 기능을 제공하는 모듈, 저수준 모듈
은 하위 기능을 실제로 구현하는것. 고수준 모듈이 저수준 모듈을 의존하면 안된다. 그러면 테스트도 어렵고 구현 변경이 어려워진다.
DIP
DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아드릴 수 있다. 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 것이다.
계층 구조에서 생각해봤을 때 도메인에서 인프라스트럭처를 의존하면 안된다. DIP를 생각해야한다. (p.76)
도메인에 interface를 집어 넣고 인프라에서 그걸 의존받아 구현하게 만들어야한다.
💡 그렇다고 무조건 DIP를 고집하진 말자 때로는 의존적인 코드를 도메인에 일부 포함하는 게 효과적일 때가 있다.
엔티티
주문, 회원, 상품과 같이 도메인의 고유한 개념을 표현.
벨류
배송지 주소를 표현하기 위한 주소Address, Money와 같은 타입이 밸류
애그리거트
엔티티와 밸류 객체를 개념적으로 하나로 묶은 것. Order Entity, OrderLine, Orderer ‘주문’ 애그리거트로 묶을 수 있다.
에그리거트가 잘 되어있으면 복잡한 도메인도 사위 수준에서 모델 간의 관계를 더 잘 파악할 수 있다. (p.99)
경계가 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함꼐 생성되는 구성요소는 한 에그리거트에 속할 가능성이 높다.
함께 변경되는 빈도가 높은 객체는 한 애그리거트에 속할 가능성이 높다. 예를 들어 주문에 상품 개수, 배송지 정보, 주문자 정보, 주문 금액 등이 될 수 있다.
흔히 ‘A가 B를 갖는다’로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉬운데, 항상 그런것은 아니다 좋은 예가 상품과 리뷰이다.
상품 상세 페이지에 들어가면 상세 정보와 리뷰 내용을 보여줘야한다는 요구사항이 있을 때 Product와 Review 엔티티가 한 애그리거트에 속한다고 생각 할 수 있으나 함께 생성되지 않고, 변경될때 한번에 변경되지 않는다. 주체도 다르다 Prodcut는 변경자가 상품 담당자라면 Review는 생성과 변경하는 주체는 고객이다.
에그리거트 루트
에그리거트에 속한 모든 객체가 일관된 상태를 유지하도록 전체를 관리하는 주체 (p.104)
트랜잭션
웬만하면 에그리거트에서 트랜잭션을 한 에그리거트만 변경하자. 응용 서비스에서 수정하도록 구현하자. 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 꼴이된다.
- 팀 표준: 팀이나 조직의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션에 실행해야 하는 경우가 있다.
- 기술 제약: 이벤트 방식을 도입할 수 없는 경우라면 다수의 트랜잭션을 일관되게 처리해야한다.
- UI 구현의 편리: 운영자의 편리함을 위해 주문 목록 화면에서 여러 주문의 상태를 한 번에변경하고 싶을 것이다. 이경우 한 트랜잭션에서 여러 주문 애그리거트의 상태를 변경해야 한다.
리포지터리와 애그리거트
애그거트는 개념상 한개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리와 애그리거트 단위로 존재한다. Order와 OrderLine을 물리적으로 각각 별도의 DB에 저장한다고 해서 각각 따로 리포지토리를 만들진 않는다. Order는 애그리거트 루트고 OrderLine은 애그리거트에 속하는 구성요소이므로 Order위한 리포지터리만 존재한다.
외래키(ID)로 참조하도록 entity를 설계하면 장점
- 애그리거트간의 의존 결합도를 낮춘다.
- 직접 참조하면 성능에 관련된 여러 고민들을 안해도된다. 지연로딩, 즉시로딩과 같은.
- 단일 DB를 안써도 된다.확장성에 좋다 에그리트별로 다른 db를 사용 할 수 있다.
ID를 이용해서 사용하면 즉시 로딩 지연로딩을 고민할 필요 없어지고 구현 복잡도도 낮아진다. 경계도 명확해지기 때문에 모델의 복잡도도 낮춰준다. (p.116)
애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 된다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해 보자. 도메인 응집도가 높아지는 효과가 있다. (p.126)
entity에서 기본생성자를 제공해야하는 이유는 DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 이용해서 객체를 생성하기 때문이다 jpa 프로바이더가 객체를 생성할 때만 사용한다.
CQRS
명령모델과 조회모델을 분리하는 패턴이다. 명령 모델은 상태를 변경하는 기능을 구현할 때 사용하고 조회 모델은 데이터를 조회하는 기능을 구현할 때 사용한다.
조회 모델을 구현할 때 JPA를 사용할 때도 있고 마이바티스를 사용할 때도 있고 JdbcTemplate을 사용할 수 있다. 맞춰서 사용하자. 꼭 Jpa만 사용하지 말자.
조회쿼리 깔끔하게 작성하는 법 Spec을 사용하면 코드를 깔-끔하게 조회 쿼리에서 사용 할 수 있다.
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);
}
}
List<OrderSummary> results = orderSummaryDao.findAll(spec);
assertThat(results).hasSize(1);
코드 출처 https://github.com/madvirus/ddd-start2
응용 서비스의 크기
크게 두 가지 방법으로 응용 서비스를 만드는데
한 응용 서비스 클래스에 한 도메인의 모든 기능 구현
- 장점
- 중복 코드를 없앨 수 있음.
- 단점
- 코드의 줄 수가 길어진다.
- 관련 없는 코드들이 많아서 코드 이해하는데 시간이 오래 걸릴 수가 있다.
- 장점
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
- 장점
- 코드라인을 깔끔하게 유지 할 수 있다.
- 단점
- 중복 코드가 있을 수 있다.
- 자칫 클랙스가 너무 많아 질 수 있다.
단점 극복
- 중복 코드는 helper클래스를 따로 두어서 해결 할 수 있다 (MemberCheckHelper.validateMember)
- 클래스 많아 지는 부분은 알딱깔짝센
- 장점
Impl이 필요한가?
오-래전에는 Dynamic Proxy를 사용해서 reflaction을 proxy가 구현됐지만 지금은 알아서 cglib로 사용하기 바이트코드 조작으로 사용하기 Proxy를 구현하기 때문에 인터페이스를 사용하여 상속 받을 일 없으면 안써도 괜찮다.
도메인 서비스
도메인 서비스는 도메인 영역에서 위치한 도메인 로직을 표현할 때 사용. 로직을 실행 할 때 주체가 모호 할 때 도메인영역에 서비스를 넣을 수 있다.
도메인에 도메인서비스를 주입하지 말자. 메소드로 주입하게 처리하자. (p.239)
도메인 로직인지 도메인 서비스 로직인지 햇갈린다면
상태를 변경하는건지 계산하는건지 생각을 해보자. 상태를 변경하면 도메인에 들어가고 계산을 하게된다면 도메인 서비스 로직에 들어가는 게 코드 유지보수하기가 좋다.
Find 함수에도 version을 강제로 증가 시킬 수 있다
애그리거트루트에 값이 변경이 안되었어도 다른 애그리거트가 변경이 된다면 버전을 업데이트 해줘야한다. 이 때 find 함수 안에 강제로 version 증가 할 수 있도록 LockModeType 인자가 있다
Offline Pessimisitic Lock
비관적인락과 낙관적인락 보다 더 엄격하게 lock을 거는 방법이다. 컨셉은 조회 할 때부터 락테이블을 만들고 지금 조회중이면 락테이블을 update를 해서 선점을 표시하고, 다른 트랜잭션이 조회를 못하도록 하는 방식이다. 이 방식은 여러 고려사항을 해야하는데 최대 lock잠금 시간을 얼마나 해야 하는지와 선점을 하지 못한 트랜잭션은 어떻게 해야할지가 고려대상이다. 선점을 하지 못한 트랜잭션이 스핀락 문제가 발생 할 수도 있으니 주의 해야한다.
바운디드 컨텍스트 p.278
모델의 경계를 결정하며 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 갖는다.
용어를 기준으로 구분한다.
만약 한 바운디드 컨텍스트에서 여러 도메인을 구현해야된다면 패키지로 쪼개서 개발을 하는 게 좋다.
Event 방식으로 구현 시 생각 할 점
- 이벤트 주체를 추가 할지 여부
- Event Entry
- 예를 들어 요구사항이 Order가 발생시킨 이벤트만 조회하기 이런식으로 요청이 왔을 때 기능을 구현을 할 수가 없다.
- Event Entry
- 포워더에서 전송 실패를 얼마나 허용할 것이냐
- 이벤트를 전송하는데 3회 이상 실패했다면 해당 이벤트는 생략하고 다음 이벤트로 넘어간다는 등의 정책이 필요하다.
- 이벤트 손실
- 이벤트 저장소를 이용하는 방식은 이벤트 발생과 이벤트 저장을 한 트랜잭션으로 처리하기 때문에 트랜잭션이 성공하면 이벤트가 저장소에 보관된다.
- 이벤트 순서
- 이벤트 발생 순서대로 외부 시스템에 전달해야 할 경우, 이벤트 저장소를 사용하는 것이 좋다.
- 이벤트 재처리
- 가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것.
이벤트가 헨들러가 멱등성을 가지면 중복처리에 대한 부담이 줄어든다.
예를들어 시스템 장애로 인해 같은 메세지가 보내져도 상관없는 시스템 개인정보의 주소가 변경된다던지 이런 시스템일 경우 중복처리의 부담감이 줄어든다.
CQRS
도입을 고민 할 때는 트래픽이 많은지와 안 많은지 또 도메인이 복 잡지 안한지를 고민을 하고 CQRS
를 고민을하면 두 모델을 유지하는 비용만높고 얻을 수는 있는 이점이 없다. 반면에 트래픽이 높은 서비스으인데 단일 모델을 고집한다면 유지 보수 비용이 높아질 수 있으니 도입을 고려 할만하다.
후기
Domain Driven Design 책을 읽어야지 하고 지금까지는 개인적으로 도메인에 대해 정말 딥하게 고민 한 적도 없었고
어떤 구조가 더 좋은 구조일까? 도메인을 어떻게 쪼개야 할까? 이런 고민을 깊게 안해 보고 DDD를 읽는 건 도움이 전혀 안된다고 생각을 해서 미루고 미루다가 그래도 최근에는 조금은 어떤 구조가 객체지향적이며 유지보수하기 좋을까라는 고민을 하고 있는 시기여서,
먼저 가볍게 코드도 많은 범균님의 DDD Start를 선택을 했었다. 이 선택은 후회가 없는 선택이였다.
만약 이 책이 아니라 이론만 가득한 책들을 봤다면 아직 "내공이 부족해서 중도 포기를 하지 않았을까"라는 생각을 하게 되었고 가볍게 지금까지 들어보기만 했던 애그리거트 애그리루트 개념에 대해 이해를 할 수가 있었고 단순히 DDD에서 끝내는 것이아니라 MSA를 고려하거나 Event를 처리 할 때 한 번씩 했었던 생각들 나는 어떻게 처리를 했는지 생각을 해 볼 수 있었고, 더욱이 범균님이 실제로 코드들을 작성하면서 했을만한 여러 고민들을 실제로 옆에서 지켜보는 듯한 느낌이 들어 나에게 많은 도움이 되는 책이였다!