[도메인 주도 개발 시작하기] 2. 아키텍처 개요
아키텍처
도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
인트라 스트럭처에 의존하면 '테스트 어려움'과 '기능 확정의 어려움'이라는 두 가지 문제가 발생한다. 이 두 문제를 해소 할 수 있는 방법은 'DIP'에 있다.
DIP
고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두 가지 문제, 즉 구현 변경과 테스트가 어렵다는 문제가 발생한다. DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. 비밀은 추상화한 인터페이스에 있다.
public interface RuleDiscounter{
Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService{
private Rulediscounter ruleDiscounter;
public CalculatediscountService(ruleDiscounter ruleDiscounter){
this.ruleDiscounter = ruleDiscounter;
}
public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
Customer customer = findCustomer(customerId);
return ruleDiscounter.applyRules(customer, orderLines);
}
...
}
public class DroolsRuleDiscounter implements RuleDiscounter{
private KieContainer kContainer;
public DroolsRuleDiscounter(){
KieServices ks = KieServices.Factory.get();
kContainer = ks.getKieClasspathContainer();
}
@Override
public Money applyRules(Customer customer, List<OrderLine> orderLines){
KieSession kSession = kCounter.newKieSession("discountSession");
try{
-코드 생략
kSession.fireAllRules();
} finally {
kSession.dispose();
}
return moeny.toImmutableMoney();
}
}
//사용할 저수준 객체 생성, 변경
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
//생성자 방식 주입
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
DIP 주의사항
DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함이다. 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.
DIP와 아키텍처
DIP를 항상 적용할 필요는 없다. 사용하는 구현 기술에 따라 완벽한 DIP를 적용하기보다는 구현 기술에 의존적인 코드를 도메인에 일부 포함하는 게 효과적일 때도 있다.
도메인 영역의 주요 구성요소
엔티티와 밸류
실제 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 같은 것이 아니다. 이 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다.
ex) 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공
public class Order{
//주문 도메인 모델의 데이터
private OrderNo number;
private Orderer orderer;
private ShippingInfo shippingInfo;
...
//도메인 모델 엔티티는 도메인 기능도 함께 제공
public void changeShippingInfo(ShippingInfo newShippingInfo){
...
}
}
도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다. 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다.
public class Orderer{
private String name;
private String email;
...
}
밸류는 불변으로 구현할 것을 권장하며, 이는 엔티티의 밸류 타입 데이터를 변경할 때는 객체 자체를 완전히 교체한다는 것을 의미한다.
public class Order{
private ShippingInfo shippingInfo;
...
//도메인 모델 엔티티는 도메인 기능도 함께 제공
public void changeShippingInfo(ShippingInfo new ShippingInfo){
checkShippingInfoChangeable();
setShippingInfo(newShippingInfo);
}
private void setShippingInfo(ShippingInfo newShippingInfo){
if(newShippingInfo == null) throw new IllegalArgumentException();
//밸류 타입의 데이터를 변경할 때는 새로운 객체로 교체한다.
this.shippingInfo = newShippingInfo;
}
}
애그리거트
도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트, 객체를 하나로 묶은 군집.
주문 - 주문, 배송지 정보, 주문자, 주문 목록, 총 결제 금액
public class Order{
...
public void changeShippingInfo(ShippingInfo newInfo){
checkShippingInfoChangeable(); //배송지 변경가능 여부 확인
this.shippingInfo = newInfo;
}
private void checkShippingInfoChangeable(){
..배송지 정보를 변경할 수 있는지 여부를 확인하는 도메인 규칙 구현
}
}
리포지터리
리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
public interface OrderRepository{
Order findByNumber(OrderNumber number);
void save(Order order);
void delete(Order order);
}
//응용 서비스
public class CancelOrderService{
private OrderRepository orderRepository;
public voic cancel(OrderNumber number){
Order order = orderRepository.findByNumber(number);
if(order == null) throw new NoOrderException(number);
order.cancel();
}
}
리포지터리를 구현하는 방법은 선택한 구현 기술에 따라 달라진다.
인프라스트럭처
표현 영역, 응용 영역, 도메인 영역을 지원한다. 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다.