CHAPTER 2. 아키텍처 개요
2. DIP
| Dependency Inversion Principle, 의존 역전 원칙
고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야 한다. 그런데, 고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두 가지 문제(구현 변경과 테스트가 어려움)가 발생한다.
DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. 고수준 모듈을 구현하려면 저수준 모듈을 사용해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존하도록 하려면 추상화한 인터페이스를 사용해야 한다.
CalculateDiscountService
입장에서 룰 적용을 Drools로 구현했는지, Java로 직접 구현했는지는 중요하지 않다.
단지, '고객 정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다'는 것이 중요할 뿐.
public interface RuleDiscounter {
public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
CalculateDiscountService
가 RuleDiscounter
를 이용하도록 바꿔보면
public class CalculateDiscountService {
private RuleDiscounter ruleDiscounter;
public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
this.ruleDiscounter = ruleDiscounter;
}
public Money calculateDiscout(List<OrderLine> orderLines, String customerId) {
Customer customer = findCustomer(customerId);
return ruleDiscounter.applyRules(customer, orderLines);
}
...
}
CalculateDiscountService
는 Drools에 의존하는 코드를 갖고 있지 않다. 단지, RuleDiscounter
가 룰을 적용한다는 것만 알 뿐.
실제 RuleDiscounter
의 구현 객체는 생성자를 통해서 전달 받는다.
룰 적용을 구현한 클래스는 RuleDiscounter
인터페이스를 상속받아 구현한다. (Drools 코드는 이해할 필요 없다.)
여기서 중요한 건 RuleDiscounter
를 상속받아 구현 한다는 것.
public class DroolsRuleDiscounter implements RuleDiscounter {
private KieContainer kContainer;
public DroolsRuleDiscounter() {
KieServices ks = KieServices.Factory.get();
kContainer = ks.getKieClasspathContainer();
}
@Override
public Money applyRule(Customer customer, List<OrderLine> orderLines) {
KieSession kSession = kContainer.newKieSession("discountSession");
try {
... 코드 생략
kSession.fireAllRules();
} finally {
kSession.dispose();
}
return money.toImmutableMoney();
}
}
구조를 보면 CalculateDiscounterService
는 더 이상 구현 기술인 Drools에 의존하지 않는다. '룰을 이용한 할인 금액 계산'을 추상화한 RuleDiscounter
인터페이스에 의존할 뿐이다.
'룰을 이용한 할인 금액 계산'은 고수준 모듈의 개념이므로 RuleDiscounter
인터페이스는 고수준 모듈에 속한다. DroolsRuleDiscounter
는 고수준의 하위 기능인 RuleDiscounter
를 구현한 것이므로 저수준 모듈에 속한다.
💡 그래서 DIP란?
DIP를 적용하면 위 그림과 같이 저수준 모듈이 고수준 모듈에 의존하게 된다. 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데,
반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Dependency Inversion Principle, 의존 역전 원칙) 라고 부른다.
또한 DIP를 적용하면 앞서 다른 영역이 인프라스트럭처 영역에 의존할 때 발생했던 두 가지 문제인 구현 교체가 어렵다는 문제와 테스트가 어려운 문제를 해소할 수 있다.
먼저 구현 기술 교체 문제를 보면, 고수준 모듈은 더 이상 저수준 모듈을 의존하지 않고 인터페이스에 의존한다.
실제 사용할 저수준 구현 객체는 아래 코드처럼 의존 주입을 이용해서 전달 받을 수 있다.
// 사용할 저수준 객체 생성
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
// 생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);
테스트에 대해 언급하기 전, CalculateDiscountService
가 제대로 동작하려면 Customer를 찾는 기능도 구현해야 한다.
이를 위한 고수준 인터페이스 CustomerRepository
를 만들었다.CalculateDiscountService
는 CustomerRepository
와 RuleDiscounter
를 사용해서 기능을 구현한다.
public class CalculateDiscountService {
private CustomerRepository customerRepository;
private RuleDiscounter ruleDiscounter;
public CalculateDiscountService(CustomerRepository customerRepository, RuleDiscounter ruleDiscounter) {
this.customerRepository = customerRepository;
this.ruleDiscounter = ruleDiscounter;
}
public Money calculateDiscount(OrderLine orderLines, String customerId) {
Customer customer = findCustomer(customerId);
return ruleDiscounter.applyRules(customer, orderLines);
}
private Customer findCustomer(String customerId) {
Customer customer = customerRepository.findById(customerId);
if(customer == null) throw new NoCustomerException();
return customer;
}
...
}
CalculateDiscountService
를 테스트하려면 CustomerRepository
와 RuleDiscounter
를 구현한 객체가 필요하다.
만약 CalculateDiscountService
가 저수준 모듈에 직접 의존했다면 저수준 모듈이 만들어지기까지 테스트를 할 수가 없었겠지만 CustomerRepository
와 RuleDiscounter
는 인터페이스이므로 대용 객체를 사용해서 테스트를 진행할 수 있다.
아래 코드는 대용 객체를 사용해서 Customer가 존재하지 않는 경우 Exception이 발생하는지 검증하는 테스트 코드이다.
public class CalculateDiscountServiceTest {
@Test(expected = NoCustomerException.class);
public void noCustomer_thenExceptionShouldBeThrown() {
// 테스트 목적의 대용 객체
CustomerRepository stubRepo = mock(CustomerRepository.class);
when(stubRepo.findById("noCustId")).thenReturn(null);
RuleDiscounter stubRule = (cust, lines) -> null;
// 대용 객체를 주입받아 테스트 진행
CalculateDiscountService calDisSvc = new CalculateDiscountService(stubRepo, stubRule);
calDisSvc.calculateDiscount(someLines, "noCustId");
}
}
-- stubRepo
: CustomerRepository
대용 객체 (Mockito라는 Mock 프레임워크를 이용)
-- stubRule
: RuleDiscounter
대용 객체 (메서드가 하 나여서 람다식으로 객체 생성)
그래서 위 테스트 코드는 CustomerRepository
와 RuleDiscounter
의 실제 구현 클래스가 없어도 CalculateDiscountService
를 테스트 할 수 있다.
결과적으로, DIP를 이용해서 고수준 모듈이 저수준 모듈에 의존하지 않도록 했기에 대용 객체를 사용해서 거의 모든 상황을 테스트 할 수 있다.
🏛️ DIP 주의사항
DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다.
DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만 보고 아래 그림과 같이 저수준 모듈에서 인터페이스를 추출하는 경우가 있다.
위 그림은 잘못된 구조인데, 도메인 영역이 구현 기술을 다루는 인프라스트럭처 영역에 의존하고 있다. 즉, 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 것이다.RuleEngine
인터페이스는 고수준 모듈인 도메인 관점이 아니라 룰 엔진이라는 저수준 모듈 관점에서 도출한 것이다.
DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.CalculateDiscountService
입장에서 봤을 때 할인 금액을 구하기 위해 룰 엔진을 사용하는지, 직접 연산하는지 여부는 중요하지 않다.
단지 규칙에 따라 할인 금액을 계산한다는 것이 중요할 뿐.
즉, '할인 금액 계산'을 추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치한다.
🏛️ DIP와 아키텍처
인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 응용 영역과 도메인 영역은 고수준 모듈이다.
인프라스트럭처 계층의 가장 하단에 위치하는 계층형 구조와 달리 아키텍처에 DIP를 적용하면 아래 그림과 같이 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 된다.
인프라스트럭처에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경 가능하다.
인프라스트럭처 영역의 EmailNotifier
클래스는 응용 영역의 Notifier
인터페이스를 상속받고 있다.
주문 시 통지 방식에 SMS를 추가해야 한다는 요구사항이 들어왔을 경우 응용 영역의 OrderService
는 변경할 필요가 없다.
요구사항이 들어왔을 때 두 통지 방식을 함께 제공하는 Notifier
구현 클래스를 인프라스트럭처 영역에 추가하면 된다.
비슷하게 Mybatis 대신 JPA를 구현 기술로 사용하고 싶다면 JPA 이용한 OrderRepository
구현 클래스를 인프라스트럭처 영역에 추가하면 된다.
📌 Reference
DDD Start! - 도메인 주도 설계 구현과 핵심 개념 익히기 - 최범균
'DDD' 카테고리의 다른 글
[DDD/도메인 주도 설계] 🏛️ chap 2. 아키텍처 개요 - 아키텍처 (0) | 2024.11.07 |
---|---|
[DDD/도메인 주도 설계] 🏛️ chap 1. 도메인 모델 시작 - 도메인 용어 (1) | 2024.10.28 |
[DDD/도메인 주도 설계] 🏛️ chap 1. 도메인 모델 시작 - 엔티티와 밸류 (4) | 2024.10.25 |
[DDD/도메인 주도 설계] 🏛️ chap 1. 도메인 모델 시작 - 도메인 모델 (3) | 2024.10.22 |