표현(UI)
응용
로직을 직접 수행하기보단, 도메인 모델에 구현된 로직을 호출 (로직 수행을 도메인 영역에 위임)
public class CancleOrderService {
@Transactional
public void cancleOrder(String orderId) {
Order order = findOrderById(orederId);
if (order == null) throw new OrderNotFoundException(orderId);
order.cancel();
}
}
도메인
인프라스트럭처
실제 구현
을 다룬다.표현, 응용, 도메인은 실제 구현 기술을 사용하는 코드를 작성하지 않고, 인프라스트럭처 영역에서 제공하는 기능을 사용한다.
일반적으로 표현 -> 응용 -> 도메인 -> 인프라스트럭처 순으로 각 계층은 이전 계층에 의존하지 않는다. 하지만 구현의 편리함을 위해 응용 -> 도메인, 인프라스트럭쳐
, 도메인 -> 인프라스트럭처
식으로 응용 영역이 인프라스트럭처 영역의 모듈에 의존하기도 한다.
예를 들어 할인 금액 계산 기능을 제공하는 응용 서비스가 있다고 해보자. 복잡한 할인 규칙 계산하기 쉽게 Drools라는 룰 엔진을 사용해 인프라스트럭처에 모듈로 구현하고, 응용 서비스에서 이를 사용한다. 이 말인 즉, 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭쳐에 의존할 수 있다는 점이다. 예를 들면 아래처럼 될 수 있다. Drools 룰 엔진 세부 구현 (인프라스트럭처)
public class DroolsRuleEngine {
private KieContainer kContainer;
public DroolsRuleEngine {
KieServices ks = KieServices.Factory.get();
kContainer = ks.getKieClasspathContainer();
}
public void evalute(String sessionName, List<?> facts) {
KieSession kSession = kContainer.newKieSession(sessionName);
try {
facts.forEach(x -> kSession.insert(x));
kSession.fireAllrules();
} finally {
kSession.dispose();
}
}
}
할인 금액 계산 서비스 (응용)
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); // Drools 특화코드 (세부 기술에 대한 지식)
facts.addAll(orderLines); // Drools 특화코드 (세부 기술에 대한 지식)
ruleEngine.evalute("discountCalculation", facts); // Drools 특화코드 (세부 기술에 대한 지식)
return money.toImmutableMoney();
}
}
위 코드에서 볼 수 있는 문제점은 다음과 같다.
- 응용 계층의 로직만으로는 테스트가 불가능하다. 인프라스트럭처 모듈이 정상 작동 해야한다. 즉, roolsRuleEngine이 완벽하게 동작해야만 CalculateDiscountService를 테스트할 수 있다.
- 구현 방식 변경과 기능 확장이 어려워 진다. 상위 계층인 인프라스트럭처 모듈을 수정하면 하위 응용 계층 로직도 수정해야한다.
고수준 모둘이 제대로 동작하려면 저수준 모듈을 사용해야 하는데, 인프라스트럭쳐의 경우 특정 기술을 직접 구현하므로 이러한 문제점이 발생하게 된다. 이를 어떻게 처리할 수 있을까?
정답은 저수준 모델이 고수준 모델에 의존하도록 바꾸는 것이고 이를 DIP(Dependency Inversion Principle, 의존 역전 원칙) 이라고 한다. 다시 한번 CalculateDiscountService를 살펴보면, discount를 얻는데 어떤 엔진을 사용했느냐는 중요하지 않다. 단지, 고객정보와 구매정보에 룰을 적용해서 할인 금액을 구한다는 것이 중요할 뿐이다. 이 부분을 추상화해서 인터페이스로 만들 수 있다.
public interface RuleDiscounter {
public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
이 인터페이스를 사용하여 CalculateDiscountService에서 DroolsRuleEngine을 제거할 수 있다.
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);
}
}
Drools를 사용하는 RuleDiscounter 구현
public class DroolsRuleDiscounter implements RuleDiscounter {
@Override
public Money applyRules(Customer customer, List<OrderLine> orderLines) {
// 세부 구현
...
}
}
CalculateDiscountService
는 더 이상 구현기술인 Drools(저수준)
에 의존하지 않고,
룰을 이용한 할인 금액 계산
을 표현하는 RuleDiscounter
인터페이스(고수준)에 의존한다.
그리고 인터페이스를 구현한 저수준 모듈은 외부에서 생성해 주입(Dependency Injection) 해주게 된다.
이와 같이 DIP를 적용함으로써 기존의 고수준 모듈에서 저수준 모듈 사용에서 오던 문제점들을 해결할 수 있게 된다.
엔티티 (ENTITY)
밸류 (VALUE)
애그리거트 (AGGREGATE)
주문
도메인은 주문 엔티티, 주문 항목 엔티티, 주문자 엔티티를 주문
애그리거트로 묶을수있다.리포지터리 (REPOSITORY)
도메인 서비스 (DOMAIN SERVICE)
할인 금액 계산
기능은 상품, 쿠폰, 회원 등급 구매 금액 등 여러 엔티티와 밸류를 필요로 하는데 이럴 때 도메인 서비스를 통해 기능을 구현.도메인 모델의 엔티티와 DB 모델의 엔티티는 차이점이 있다.
DB 엔티티는 속성을 컬럼으로 밖에 표현할 수 없는데 도메인 모델의 엔티티는 밸류 타입을 사용할 수 있다.
변경
이란 밸류 객체 자체를 교체하는건데, 테이블 분리라면 값 변경일 뿐이고 이는 밸튜 타입 구현에 권장되는 방법인 불변
에 위배됨데이타베이스 분야에서 개체 또는 엔티티(Entity)라고 하는 것은 데이타베이스에 표현하려고 하는 유형, 무형의 객체로서 서로 구별되는 것을 뜻한다. 이 개체는 현실 세계에 대해 사람이 생각하는 개념이나 정보의 단위로서 의미를 가지고 있다. 이것은 컴퓨터가 취급하는 화일의 레코드(record)에 대응한다. 이 개체는 그 단독으로 존재할 수 있으며, 정보로서의 역할을 한다. 하나의 개체는 하나 이상의 속성, 즉 애트리뷰트(attribute)로 구성되고 각 속성은 그 개체의 특성이나 상태를 기술해 준다.
도메인이 커질수록 개발할 도메인 모델도 커지게 되고, 많은 엔티티와 벨류가 생기면서 모델이 점점 더 복잡해진다. 이렇게 도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 한개 엔티티와 벨류에 집중하게 되는 경우가 발생한다.
지도를 볼 때 매우 상세하게 나온 대축적 지도를 보면 큰 수준에서 어디에 위치하고 있는지 이해하기 어려우므로 큰 수준에서 보여주는 소축적 지도를 함꼐 봐야 현재 위치를 보다 더 정확하게 이해할 수 있다. 이와 비슷하게 도메인 모델도 개별 객체가 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델과 개별 모델을 이해하는데 도움이 된다. 이게 바로 애그리거트(AGGREGATE) 이다.
애그리거트는 관련 객체를 하나로 묶어 도메인 모델을 상위 수준에서 이해하는 것을 돕는다.
주문
애그리거트는 주문
,주문자
,배송정보
등을 포함한다.애그리거트는 해당 군집에 속한 객체들을 관리하는 루트 엔티티를 갖는다. (애그리거트 루트)
OOP에서 객체의 캡슐화와 애그리거트를 대조해 생각해보면 좀 더 이해가 쉽다.
- 객체의 필드(속성), 메소드를 하나로 묶고, 실제 구현 내용을 외부에 감추는 것을 말한다. => 애그리거트 루트이외 하위 엔티티 및 밸류는 노출하지 않음
- 외부 객체는 객체 내부의 구조를 얻지 못하며 객체가 노출해서 제공하는 필드와 메소드만 이용할 수 있다. => 애그리거트 루트를 통해서만 도메인 기능 호출 가능
- 필드와 메소드를 캡슐화하여 보호하는 이유는 외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 하는데 있다. => 애그리거트에 대한 조작을 의도한 방법만으로만 가능하게 함
도메인 모델을 지속적으로 사용하려면 RDBMS, NoSQL, 파일 등 물리적 저장소에 저장(영속화)해야한다. 이를 위한 도메인 모델이 리포지터리이다.
public inferface OrderRepository {
public Order findByNumber(OrderNumber number);
public void save(Order order);
public void delete(Order order);
}
public class CancelOrderService {
private OrderRepository orderRepository;
public void cancel(OrderNumber number) {
// 리포지터리를 통해 도메인 객체 조회
Order order = orderRepository.findByNumber(number);
if (order == null) throw new NoOrderException(number);
// 도메인 객체에 구현된 취소 기능 실행
order.cancel();
}
}
@Configuration
public class OrderServiceConfig {
@Autowired
private OrderRepository orderRepository;
@Bean
public CancelOrderService cancelOrderService() {
return new CancelOrderService(orderRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public JpaOrderRepository orderRepository() {
return new JpaOrderRepository();
}
@Bean
public LocalContainerEntityManagerFactoryBean emf() {
...
}
}
응용 서비스와 리포지터리는 다음과 같은 이유로 밀접한 연관이 있다.
리포지터리의 사용 주체가 응용 서비스이기 때문에 리포지터리는 응용 서비스가 필요로 하는 메서드를 제공
void save(Aggregate agg)
Aggregate findById(SomeId id)
delete
, counts
등 추가 기능 제공
기능 구현에 필요한 도메인 객체를 Repository에서 가져와 도메인 로직을 실행하거나 신규 도메인 객체를 생성해서 Repository에 저장한다.
표현, 응용, 도메인 영역을 지원하는 영역이다.
인프라스트럭처를 직접 사용하는 것보다 각자의 영역에서 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 좋다.
무조건 인프라스트럭처에 대한 의존을 없애는 것이 좋은 것은 아니다.