아키텍처는 "표현", "응용", "도메인", "인프라스트럭처" 의 네 영역이다.
public class CancelOrderService {
@Transactional
public void cancelOrder(String orderId){
Order order = findOrderById(orderId);
if(order == null) throw new OrderNotFoundException(orderId);
order.cancel();
}
}
도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
예를 들어, 응용 영역에서 DB에 보관된 데이터가 필요하면 인프라스트럭처 영역의 DB 모듈을 사용해서 데이터를 읽어온다.
응용 영역과 도메인 영역은 DB나 외부 시스템 연동을 위해 인프라스트럭처의 기능을 사용하므로 이런 계층 구조를 사용하는 것이 직관적으로 이해하기 쉽다. 하지만, 짚고 넘어가야 할 것이 있는데 바로 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 점이다.
🙏 도메인 가격 계산 규칙을 예로 들어보자. 할인 금액 계산 로직이 복잡해지면 객체 지향으로 구현하는 것 보다 "룰 엔진"을 사용하는 것이 알맞을 때가 있다.
public class CalculateDiscountService{
private DroolsRuleEngine ruleEngine;
public CalculateDiscountService(){
ruleEngine = new DroolsRuleEngine();
}
public Money calculateDiscount(List<OrderLine> orderLines, String customerId){
Customer customer = findCustomer(customerId);
MutableMoney money = new MutableMoney(0);
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);
ruleEngine.evalute("discountCalculation", facts);
return money.toImmutableMoney();
}
}
CalculateDiscountService가 겉으로는 인프라스트럭처의 기술에 직접적인 의존을 하지 않는 것처럼 보여도 실제로는 Drools라는 인프라스트럭처 영역의 기술에 완전하게 의존하고 있다. 이런 상황에서 Drools가 아닌 다른 구현 기술을 사용하려면 코드의 많은 부분을 고쳐야 한다..
"테스트 어려움"과 "기능 확장의 어려움"이라는 문제가 발생한다.
이 두가지 문제를 해소 할수 있는 방법은 "DIP"를 적용하는 것이다.
CalculateDiscountService는 고수준 모듈이다. 고수준 모듈의 기능을 구현하려면 여러 하위 기능이 필요하다.
고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다. 그런데, 고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두가지 문제(구현 변경과 테스트가 어려움)가 발생한다.
DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. ( 추상화 인터페이스를 사용하면 된다.)
public interface RuleDiscounter{
public Money applyRules(Custome customer, List<OrderLine> orderLines);
}
CalculateDiscountService가 RuleDiscounter를 이용하도록 변경
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);
}
}
-> CalculateDiscountService는 더 이상 구현 기술인 Drools에 의존하지 않는다.
룰을 이용한 할인 금액 계산을 추상화한 RuleDiscounter 인터페이스에 의존할 뿐이다.
"룰을 이용한 할인 금액 계산"은 고수준 모듈의 개념이므로 RuleDiscounter 인터페이스는 고수준 모듈에 속한다. DroolsRuleDiscounter는 고수준의 하위 기능인 RuleDiscounter를 구현한 것이므로 저수준 모듈에 속한다.
DIP를 적용하면 앞서 다른 영역이 인프라스트럭처 영역에 의존할 때 발생했던 두 가지 문제인 구현 교체가 어렵다는 문제와 테스트가 어려운 문제를 해소할 수 있다.
//사용할 저수준 객체 생성
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
//생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
//사용할 저수준 구현 객체 변경
RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();
//사용할 저수준 모듈을 변경해도 고수준 모듈을 수정할 필요가 없다.
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 응용 영역과 도메인 영역은 고수준 모듈이다.
엔티티 : 주문(Order), 회원(Member), 상품(Product)과 같이 도메인의 교유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
벨류 : 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용된다.
애그리거트 : 관련된 엔티티와 밸루 객체를 개념적으로 하나로 묶은 것이다. 예를 들어, 주문과 관련된 Order 엔티티, OrderLine밸류, Order 밸류 객체를 '주문' 애그리거트로 묶을 수 있다.
리포지터리 : 도메인 모델의 영속성을 처리한다. 예를 들어, DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다.
도메인 서비스 : 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 할 경우 도메인 서비스에서 로직을 구현한다.
애그리커드를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있다.
(개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현할 수 있게 되며, 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있게 된다. )
✔ 응용 서비스와 리포지터리
public class CancelOrderService{
private OrderRepository orderRepository;
@Transaction //응용서비스는 트랜잭션을 관리한다.
public void cancel(OrderNumber number){
Order order = orderRepository.findByNumber(number);
if(order == null) throw new NoOrderException(number);
order.cancel();
}
}
✔ 표현 영역
✔ 인프라스트럭처
표현 영역, 응용 영역, 도메인 영역을 지원한다. 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다.
DIP에서 언급한 것처럼 도메인 영역과 응용 영역에서 인프라스트럭처의 기능을 직접 사용하는 것보다 이 두 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어 준다.
하지만 무조건 인프라스트럭처에 대한 의존을 없애는 것이 좋은 것이 아니다. (스프링은 @Transactional / JPA는 @Entity나 @Table과 같은 JPA 전용 애노테이션을 도메인 모델 클래스에 사용하는 것이 효율적이다.)
응용 영역과 도메인 영역이 인프라스트럭처에 대한 의존을 완전히 갖지 않도록 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다.