주문 등록
, 주문 취소
, 상품 상세 조회
public class CancelOrderService {
@Transactional
public void cancelOrder(String orderId) {
Order order = findOrderById(orderId);
if (order == null) {
throw new OrderNotFoundException(orderId);
}
}
...
}
Order
, OrderLine
, ShippingInfo
배송지변경
, 결제완료
, 주문 총액 계산
과 같은 핵심 로직을 도메인에서 구현evalute()
에 값을 주면 별도 파일로 작성한 규칙을 이용해 연산을 수행하는 코드.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();
}
}
}
DroolsRuleEngine
을 이용해 응용영역의 코드를 작성했다.CalculateDiscountService
만 테스트하기 힘들다.RuleEngine
이 완벽하게 작동할때만 가능하다.calculateDiscount()
가 겉으로는 인프라 스트럭처의 기술에 의존하지 않는 것처럼 보여도, 해당 기술에 완전하게 의존하고 있다.discountCalculation
는 세션이름으로 인프라 스트럭처에서 변경된다면 함께 변경되어야 한다.MutableMoney
는 룰 적용 결과값을 보관하기 위해 추가한 타입으로, 다른 방식을 이용했다면 필요없는 타입이다.public class CalculateDiscountService {
private DroolsRuleEngine ruleEngine;
public CalculateDiscountService() {
ruleEngine = new DroolsRuleEngine();
}
public Money calculateDiscount(OrderLine orderLine, String customerId) {
Customer customer = findCustomer(cutomerId);
// Drolls에 특화된 코드: 연산 결과를 받기 위해 추가한 타입
MutableMoney money = new MutableMoney(0);
// Drools에 특화된 코드: 룰에 필요한 데이터(지식)
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);
// Drools에 특화된 코드: Drools의 세션 이름
ruleENgine.evalue("discountCalculation", facts);
return money.toImmutableMoney();
}
...
}
public interface RuleDiscounter {
public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService {
private RuleDiscounter ruleDiscounter;
public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
this.ruleDsicounter = ruleDiscounter;
}
public Money calculateDiscount(OrderLine orderLine, String customId) {
Customer customer = findCustomer(customId);
return ruleDiscounter.applyRules(customer, orderLines);
}
...
}
RuleDiscounter
가 룰을 적용한다는 것만 알고있다.RuleDiscounter
의 구현 객체는 생성자로 주입받는다.public class DroolsRuleEngine {
private KieContainer kContainer;
public DroolsRuleEngine() {
KieServices ks = KieServices.Factory.get();
KContainer = ks.getKieClasspathContainer();
}
@Override
public Money calculateDiscount(Customer customer, List<OrderLine> orderLines) {
KieSession kSession = kContainer.newKieSession(sessionName);
try {
facts.forEach(x -> kSession.insert(x));
kSession.fireAllRules();
} finally {
kSession.dispose();
}
return money.toImmutableMoney();
}
}
할인 금액 계산
이 그것이다.Order
) 엔티티는, 주문과 관련된 데이터 뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공한다.public class Order {
// 주문 도메인 모델의 데이터
private OrderNo number;
private Orderer orderer;
private ShippingInfo shippingInfo;
...
// 도메인 모델의 엔티티는 도메인 기능도 함께 제공
public void changeShippingInfo(ShippingInfo newShippingInfo) {
...
}
}
Orderer
)는 벨류 타입을 이용해 표현 가능하다.public class Orderer {
private String name;
private String email;
...
}
public class Order {
private ShippingInfo shippingInifo;
...
// 도메인 모델 엔티티는 도메인 기능도 함께 제공
public void changeShippingInfo(ShippingInfo newShippinginInfo) {
checkShippingInfoChangeable();
setShippingInfo(newShippingInfo);
}
private void setShippingInfo(ShippingInfo newShippingInfo) {
if (newShippingInfo == null) {
throw new IllegalArgumentException();
}
// 밸류 타입의 데이터를 변경할 때는 새로운 객체로 교체한다.
this.shippingIinfo = newShippingInfo;
}
public class Order {
...
public void changeShippingInfo(ShippingInfo newInfo) {
checkShippingInfoChangeable(); // 배송지 변경 가능 여부 확인
this.shippingInfo = newInfo;
}
private void checkShippingInfoChangeable() {
... 배송지 정보를 변경할 수 있는지 여부를 확인하는 도메인 규칙 구현
}
}
Order
)가 애그리거트에 속한 객체를 관리한다.checkShippingInfoChangeable()
는 배송지를 변경할 수 있는지 확인한다.Order
를 통하지 않고 100000ShippingInfo
를 변경할 수 있는 방법을 제공하지 않는다.Order
를 사용해야 하므로, 반드시 Order
가 구현한 도메인 로직을 항상 따라야 한다.애그리거트를 구현할 때는 고려할 것이 많다. 애그리거트를 어떻게 구성했느냐에 따라 구현이 복잡해지기도 하고 트랜잭션 범위가 달라지기도 한다. 또한 선택한 구현 기술에 따라 애그리거트 구현이 제약이 생기기도 한다.
public interface OrderRepsitory {
public Order findByNumbre(OrderNumber number);
public void save(Order order);
public void delete(Order order);
...
}
OrderRepsitor
의 메서드를 보면, 대상을 찾고 저장하는 단위가 애그리거트 루트인 Order
인 것을 확인할 수 있다.Order
는 애그리거트에 속한 모든 객체를 포함하고 있으므로, 결과적으로 애그리거트 단위로 저장하고 조회한다.public class CancelOrderService {
prviate OrderRepository orderRepository;
public void cancel(OrderNumber number) {
Order order = orderRepository.findByNumber(number);
if (order == null) throw new NoOrderException(number);
order.cancel();
}
... DI 등의 방식으로 OrderRepository 객체 전달
}
OrderRepsitory
OrderRepository
구현 클래스@Configuration
public class OrderServiceConfig { // 응용 서비스 영역 설정
@Autowired
private OrderRepository orderRepository;
@Bean
public CancelOrderService cancelOrderService() {
return new CancelOrderService(orderRepsitory);
}
}
@Configuration
public class RepositoryConfig { // 인프라스트럭처 영역 설정
@Bean
public JpaOrderRepository orderRepository() {
return new JpaOrderRepository();
}
@Bean
public LocalContainerEntityManagerFactoryBean emf() {
...
}
}
public class CancelOrderService{
private OrderRepository orderRepository;
@Transcation // 응용 서비스는 트랜잭션을 관리한다.
public void cancel(OrderNumber number) {
Order order = orderRepository.findByNumber(number);
if (order == null) throw new NoOrderException(number);
order.cancel();
}
...
}