아키텍처는 크게 4가지 영역으로 구분이 가능하다.
- 표현
- 응용
- 도메인
- 인프라스트럭처
웹 어플리케이션에서의 표현 영역은 Http 요청과 같은 방식을 통해 사용자로부터 요청을 인입 받게되고,
해당 요청을 응용 영역에서 요구하는 형식으로 변환하여 응용 영역에 전달하게 된다.
@RestController
@RequiredArgsConstructor
public class ProductController {
private final InvestmentService investmentService;
private final ModelMapper modelMapper;
@PostMapping("/v1/products/{productId}/investments")
public void addInvestment(
@PathVariable Long productId,
@RequestHeader("USER-ID") Long userId,
@Validated @RequestBody InvestmentParticipationDTO investmentParticipationDTO) {
investmentParticipationDTO.setUserId(userId);
investmentService.invest(
productId, modelMapper.map(investmentParticipationDTO, Investment.class));
}
}
위의 코드에서와 같이 컨트롤러에서는 사용자의 요청을 받아서 응용 영역에 전달하여
포맷 변환(Spring - Message Converter에 의해서 처리)이 되고 있는 코드의 예시이다.
응용 영역에서는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
@Service
@RequiredArgsConstructor
public class InvestmentService {
private final ProductService productService;
private final InvestmentRepository investmentRepository;
public void invest(Long productId, Investment investment) {
Product product =
productService.findProduct(productId).orElseThrow(ResourceNotExistException::new);
investment.invest(product);
investmentRepository.save(investment);
}
}
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Investment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "investment_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
private Long userId;
private Long investingAmount;
private LocalDateTime investingAt;
public void invest(Product product) {
product.addInvestment(this);
this.product = product;
this.investingAt = LocalDateTime.now();
}
}
위의 코드에서는 응용 영역에서 직접 로직 수행을 하지 않고 도메인 모델에 로직 수행을 위임하는 코드의 예시이다.
또한 최종적으로는 인프라 영역에 수정된 값을 넘겨주는 것을 확인할 수 있다.
인프라 스트럭처 영역은 구현 기술에 대한 것을 다룬다.
계층 구조는 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다.
(ex. 표현 계층은 응용 계층에 의존하고 응용 계층은 도메인 계층에 의존하지만, 반대로 인프라 스트럭처 계층이 도메인 또는 응용 계층에 의존하지 않는다.)
계층 구조로 구현을 하게 되는 경우 종종 계층 구조의 의존이 역전되게 되는데 이를 해결하기 위해
사용하는 방식이 DIP(Dependency Inversion Principle) 이다.
@Service
@RequiredArgsConstructor
public class CalculateDiscountService {
private final CustomerService customerService;
private final DroolsRuleEngine ruleEngine;
public void calculateDiscount(List<OrderLine> orderLines, String customerId){
Customer customer = customerService.find(customerId);
...
MutableMoney money = new MutableMoney(0);
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);
ruleEngine.evaluate("discountCalculation", facts);
return money.toImmutableMoney();
}
}
위 코드와 같은 예시가 있다고 가정했을 때,
해당 코드는 처음 봤을 때 하위 계층에 대한 의존이 없는 것처럼 느껴질 수도 있지만
할인 정책을 담당하는 코드를 더이상 DroolsEngine이 아닌 다른 기술을 이용하여 수정된다고 한다면
해당 경우, 수정되어야 하는 대상은 Repository 뿐만 아니라 Service 또한 수정이 되어야 한다.
이를 개선하기 위해서 로직을 추상화해본다면 "고객 정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다"라는 로직을 추상화 할 수 있다. 이를 이용해 개선해본다면 아래의 코드와 같다.
public interface RuleDiscounter {
public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class DroolsRuleDiscounter implements RuleDiscounter {
@Override
public Money applyRule(Customer customer, List<OrderLine> orderLines) {
...
return money.toImmutableMoney();
}
}
public class CalculateDiscountService {
...
public Money calculateDiscount(List<OrderLines> orderLines, String customerId) {
...
return ruleDiscounter.applyRules(customer, orderLines);
}
}
위와 같이 DIP를 적용하게 되면 저수준 모듈이 고수준 모듈에 의존하게 된다.
'DDD' 카테고리의 다른 글
DDD vs Transaction Script (0) | 2021.04.08 |
---|---|
[DDD Start] 1. 도메인 모델 (0) | 2021.04.06 |