기존에 아는 내용도 많기 때문에 상세히 정리는 하지 않고 몰랐던 부분 & 기록할만한 부분만 정리한다. 참고로 책 내용은 정말 알차고 좋다.
도메인: 구현해야할 소프트웨어의 대상.
도메인 모델을 표현할 때 클래스 다이어그램이나 상태 다이어그램과 같은 UML 표기법만을 사용해야 하는 것은 아니다. 도메인을 이해하는데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다.
객체 기반 모델을 이용해서 도메인을 표현했다면 객체 지향 언어를 이용해서 개념 모델에 가깝게 구현할 것이고 수학적인 모델을 사용했다면 함수를 이용해서 도메인 모델과 유사한 모델을 만들 수 있다.
표현계층: 사용자의 요청을 처리하고 사용자에게 정보를 보여준다.
응용계층: 사용자가 요청한 기능을 실행하는데 업무로직을 직접 구현하진 않으며 도메인 계층을 조합해서 기능을 실행한다.
도메인 계층: 시스템이 제공할 도메인 규칙을 제공한다.
인프라스트럭쳐 계층: 디비나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
코드는 상세한 모든 내용을 다루고 있기 때문에 코드를 이용해서 전체 소프트웨어를 분석하려면 많은 시간을 투자해야 한다. 전반적인 기능 목록이나 모듈 구조, 빌드 과정은 코드를 보고 직접 이해하는 것보다 상위 수준에서 정리한 문서를 참조하는 것이 소프트웨어 전반을 빠르게 이해하는 데 도움이 된다. 전체 구조를 이해하고 더 깊게 이해할 필요가 있는 부분을 코드로 분석해 나가면 된다.
엔티티의 가장 큰 특징은 식별자를 갖는 다는 것이다.
알맞은 영어 단어를 찾는 것은 쉽지 않은 일이지만 시간을 들여 찾는 노력을 해야 한다. 도메인에 어울리지 않는 단어를 사용하면 코드는 도메인과 점점 멀어지게 된다. 도메인 용어에 알맞는 단어를 찾는 시간을 아까워하지 말자.
DIP (의존역전원칙)를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.
애그리거트는 관련 객체를 하나로 묶은 군집이다. 예를들어, 주문이라는 도메인 개념은 주문, 배송지 정보, 주문자 등등의 하위 모델로 구성되는데 이 하위개념을 표현한 모델을 하나로 묶어서 '주문'이라는 상위 개념으로 표현할 수 있다.
애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 된다. 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다. (예를들어 주문이라는 엔티티를 루트 엔티티로 볼 수 있다)
무조건 인프라스트럭처에 대한 의존을 없애는 것이 좋은 것은 아니다. 예를들어, 응용 서비스는 트랜잭션 처리를 위해 스프링이 제공하는 @Transactional을 사용하는것이 편리하다. 또한 엔티티에 @Table 을 쓰는것도 JPA 전용 애노테이션을 사용하는 것이므로 레포지토리 영역과 의존이 생기는 것이지만 구현의 편리함이 있다.
무조건 의존을 갖지 않도록 하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다.
참고로 도메인이 크다면 하위 도메인별로 모듈을 나눌 수 있다.
애그리거트는 복잡한 모델을 관리하는 기준을 제공하는데 경계를 갖는다. 흔히 A가 B를 갖는다 라고하면 같은 애그리거트에 속한다고 생각할 수 있는데 항상 그런것은 아니다. 예를들어, 상품과 리뷰를 생각해보면 상품 상세 페이지에 들어가면 상품 상세 정보와 함께 리뷰 내용이 보여지긴 하지만 상품과 리뷰는 함께 생성되지도 않고 함께 변경되지도 않는다.
처음 도메인 설계를 시작하면 큰 애그리거트로 보이는것이 많지만 경험이 생긴다면 실제 애그리거트의 크기는 작아진다고 한다. 저자분의 경험에 따르면 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물게 존재한다고 한다.
애그리거트의 속한 모든 객체가 일관된 상태를 유지하게끔 관리를 할 주체라고 할 수 있다.
애그리거트는 서로 최대한 독립적이어야 한다.
필드를 이용한 애그리거트 참조는 다음과 같은 문제를 야기할 수 있다.
- 편한 탐색 오용: 한 애그리거트 내부에서 다른 애그리거트에 접근할 수 있으면 구현이 쉬워진다는 것 때문에 다른 애그리거트의 상태를 변경하는 유혹에 빠지기 쉽다.
- 성능에 대한 고민: 지연, 즉시 로딩 관련해서 JPQL 등 쿼리의 로딩 전략을 결정해야 한다.
- 확장 어려움: 예를들어, 하위 도메인마다 다른 종류의 데이터베이스를 쓸 수 있음.
응용 서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는 것이므로 표현 영역 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해주는 창구(파사드) 역할을 한다.
응용 서비스가 도메인 로직을 일부 구현하면 코드 품질에 안좋은 영향을 미친다. 이유로는,
- 코드의 응집성이 떨어진다. (도메인 로직을 파악하기 위해 여러 영역을 분석해야 한다)
- 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
소프트웨어의 중요한 경쟁 요소 중 하나는 변경의 용이성인데, 변경이 어렵게 된다는 것은 그만큼 소프트웨어의 가치가 떨어지는 것이다. 소프트웨어의 가치를 높이려면 도메인 로직을 도메인 영역에 모아서 코드 중복이 발생하지 않도록 하고 응집도를 높여야 한다.
역할정리
- 도메인 객체 간의 실행흐름 제어
- 트랜잭션 처리
한 클래스에 코드가 모이기 시작하면 엄연히 분리하는 것이 좋다. 구분되는 기능별로 서비스 클래스를 구현하는 방식은 한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현한다.
각 응용 서비스에서 공통되는 로직을 별도의 Helper 클래스로 분리할 수 있다.
한 애그리거트에 넣기에 애매한 도메인 기능을 특정 애그리거트에서 억지로 구현하면 안 된다.
도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다. 상태없이 로직만 구현한다는 것이다.
예를들어, 결제금액 계산 로직을 보면 할인 정책이 들어가게 되면 특정 애그리거트에 넣기가 애매해진다.
이름을 DistcoutCalculationService 라고 짓게 되면 도메인의 의미가 드러나는 용어를 사용하는 것이다.
애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다.
public class Order {
public void calculateAmounts(DiscoutCalcuationService disCalSvc, MemberGrade grade) {
// 중략...
disCalSvc.calculateDiscountAmounts(this.orderLines, this.coupons, grade);
}
}도메인 서비스 객체를 애그리거트에 의존 주입하면 안된다. Order가 제공하는 모든 기능에서 DiscountCalculationService를 필요로 하는 것도 아니고 저장대상도 아니다.
애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다. 예를들어 계좌이체의 경우 다음과 같이 할 수 있다.
public class TransferService {
public void transfer(Account fromAcc, Account toAcc, Money amounts) {
fromAcc.withdraw(amounts);
toAcc.credit(amounts);
// 중략
}
}응용 서비스는 두 Account 애그리거트를 구한 뒤에 해당 도메인 영역의 TransferService를 이용해서 계좌 이체 도메인 기능을 실행할 것이다.
도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지는 않는다. 트랜잭션 처리와 같은 로직은 응용 로직이므로 도메인 서비스가 아닌 응용 서비스에서 처리해야 한다.
특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 계산하는지 검사해보면 된다. 예를들어, 계좌 이체 로직은 계좌 애그리거트의 상태를 변경한다. 결제 금액 로직은 주문 애그리거트의 주문 금액을 계산한다. 이 두 로직은 각각 애그리거트를 변경하고 값을 계산하는 도메인 로직이다.
도메인 서비스는 도메인 로직을 실행하므로 위치는 다른 도메인 구성요소와 동일한 패키지에 위치한다.
도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 model, service, repository와 같이 하위 패키지를 구분해서 위치시켜도 된다.
논리적으로 같은 존재 처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다. '상품'이라는 도메인은 재고관리 측면에서는 실존하는 개별 객체를 추적하기 위한 목적으로 사용되고, 카탈로그 측면에서는 상품명, 이미지 가격 등 상품 정보위주일 수 있다. (등등)
이렇게 구분되는 경계를 가는 컨텍셔트를 DDD에서는 Bounded Context라고 한다.
한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 갖는다.
Bounded Context가 도메인 모델만 포함하는 것은 아니다. 도메인 모델 뿐만 아니라 도메인 기능을 사용자에게 제공하는데 필요한 표현,응용,인프라 영역을 포함한다. 모든 Bounded Context를 반드시 도메인 주도로 개발할 필요는 없다. 상품 리뷰는 복잡한 도메인 로직을 갖지 않기 때문에 CRUD 방식으로 (dao와 데이저우심의 밸류객체를 이용) 구현해도 유지보수하는데 큰 문제가 없다.