표현(Presentation), 응용(Application), 도메인(Domain), 인프라스트럭처(Infrastructure) 는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역이다.
네 영역 중 표현 영역(Application 또는 UI영역)은 사용자의 요청을 해석해 응용(Application) 서비스에 전달하고 응용 영역의 처리 결과를 다시 사용자에게 보여준다.
도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않고, 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해 필요한 기능을 기발한다.
응용 영역에서 인프라스트럭처의 DB 모듈을 사용해 DB 데이터를 활용하거나, 메일 발송을 위해 인프라스트럭처 영역의 STMP 연동 모듈을 사용하는 것이 그 예이다.
표현 영역, 응용 영역, 도메인 영역, 인프라스트럭처 영역을 구성할 때 많이 사용하는 아키텍처는 아래와 같은 계층 구조를 따른다.
계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고, 하위 계층은 상위 계층에 의존하지 않는다.
계층 구조를엄격하게 적용한다면 상위 계층은 바로 아래의 계층에만 의존을 가져야 하지만, 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다.
ex) 응용 계층은 바로 아래 계층인 도멩니 계층에 의존하지만, 외부 시스템과의 연동을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 함
이러한 구조는 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 문제가 발생한다.
인프라스트럭처에 의존하면 "테스트의 어려움"과 "기능 확장의 어려움"이라는 두가지 문제가 발생한다.
이 두 문제는 DIP를 통해 해결할 수 있다.
위 그림에서 CaculateDiscountService는 고수준 모듈로, '가격 할인 계산' 이라는 기능을 구현한다. 고수준 모듈은 의미 있는 단일 기능을 제공하는 모둘이다.
이 고소준 모듈은 1. 고객 정보를 구한다. 2. 룰을 이용해서 할인 금액을 구한다. 라는 두 가지 저수준 모듈의 하위 기능들에 의존한다.
하지만 고수준 모듈이 저수준 모듈을 사용하면 앞서 언급한 두가지 문제, 구현 변경과 테스트가 어렵다는 문제가 발생한다.
💡 DIP에서는 이 문제를 해결하기 위해 추상화한 인터페이스를 사용하여 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.
아래 [그림2.9]와 같이 DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존하게 된다.
즉, CaculateDiscountService가 구현 기술에 의술인 Drools에 의존하지 않고 추상화한 RuleDiscounter 인터페이스에 의존하게 된다.
🏷️ DIP
고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존하여 DIP(Dependency Inversion Principle) , 의존 역전 원칙이라고 한다.
고수준 모듈이 더이상 저수준 모듈에 의존하지 않고, 구현을 추상화한 인터페이스에 의존하면 아래와 같이 구현 기술(저수준 모듈)을 변경하더라도 고수준 모듈을 수정할 필요가 없다.
// 사용할 저수준 객체 생성
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
// 생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDisocuntService(ruleDiscounter);
// 사용할 저수준 구현 객체 변경
RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();
// 사용할 저수준 모듈을 변경해도 고수준 모듈을 수정할 필요가 없음
CalculateDiscountService disService = new CalculateDisocuntService(ruleDiscounter);
public class CaculateDiscountServiceTest {
@Test
public void noCustomer_thenExpectionShouldBeThrown() {
// 테스트 목적의 대역 객체
CustomerRespotiroy stubRepo = mock(CustomerRepository.classs);
when(stubRepo.findById("noCustId")).thenReturn(null));
RuleDiscounter stubRule = (cust, lines) -> null;
// 대용 객체를 주입 받아 테스트 진행
CalculateDiscountService calDisSvc =
new CalculateDiscountService(stubRepo, stubRule);
assertThrows(NoCustomerException.class,
() -> calDisSvc.calculateDiscount(someLines, "noCustId"));
}
DIP를 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들인다면 아래와 같이 저수준 모듈에서 인터페이스를 추출하여 잘못 적용하는 상황이 발생한다.
위의 구조에서 도메인 영역은 구현 기술을 다루는 인프라스트럭처 영역에 의존하고 있다. 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 셈이다.
DIP의 핵심은 고수준 모듈이 저수둔 모듈에 의존하지 않도록 하기 위함임을 명심해야한다.
따라서 DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 한다.
위의 예제를 DIP에 따라 잘 구성하면 아래와 같아진다.
보통의 인프라스트럭처 계층이 가장 하단에 위치하는 계층형 구조와 달리 아키텍처에 DIP를 적용하면 아래 그림과 같이 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 된다.
그림 [2.13] 에서 인프라스트럭처 영역의 EmailNotifier 클래스는 응용 영역의 Notifier 인터페이스를 상속받고 있다.
주문 시 통지 방식에 SMS(문자)를 추가해야한다는 요구사항이 추가되어도 응용 영역의 OrderService는 변경할 필요가 없다.
그림 [2.14]와 같이 두 통지 방식을 함께 제공하는 Notifier 구현 클래스를 인프라스트럭처 영역에 추가하기만 하면 된다.
비슷하게 Mybatis 대신 JPA를 구현 기술로 변경하여도 OrderRepository를구현한 JPA 클래스를 인프라스트럭처 영역에 추가하면 된다.
그러나 DIP를 항상 적용할 필요는 없다.