계층 구조 아키텍처
네 영역을 구성할 때 많이 사용하는 아키텍처는 아래와 같은 계층 구조입니다.
계층 구조는 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않습니다.
하지만, 편리함을 위해 아래와 같이 응용 계층에서 인프라스트럭처 계층에 직접 의존하는 계층 구조를 가지기도 합니다.
응용, 도메인 영역이 DB나 외부 시스템 연동을 위해 인프라스트럭처의 기능을 사용하므로, 위의 구조는 직관적으로 이해하기 쉽습니다. 하지만, 이 경우
표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속되게 됩니다.
도메인의 "가격 계산 규칙"을 예시로 들어보겠습니다.
우선, `Drools`라는 룰 엔진을 통해 계산 로직을 수행하는 인프라스트럭처 영역의 코드입니다.
public class DroolsRuleEngine {
private KieContainer kieContainer;
public DroolsRuleEngine() {
KieServices kieServices = KieServices.Factory.get();
kieContainer = kieServices.getKieClasspathContainer();
}
public void evalute(String sessionName, List<?> facts) {
KieSession kSession = kieContainer.newKieSession(sessionName);
try {
facts.forEach(x -> kSession.insert(x);
kieSession.fireAllRules();
} finally {
kieSession.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);
facts.addAll(orderLines);
ruleEngine.evalute("discountCalculation", facts);
return money.toImmutableMoney();
}
...
}
이 `CalculateDiscountService`에는 두 가지 문제점이 존재합니다.
1. `CalculateDiscountService`만 테스트하기 어렵다.
`CalculateDiscountService`를 테스트하기 위해서는 `RuleEngine`이 완벽하게 동작해야 합니다. 이 클래스와 관련 설정 파일을 모두 만든 이후에 비로소 올바르게 동작하는지 확인할 수 있습니다.
2. 구현 방식을 변경하기 어렵다.
위 코드의 `calculateDiscount` 메서드를 보면, `Drools`에 특화되어 있는 코드를 사용 중인 것을 확인할 수 있습니다. 만약, `Drools`의 세션 이름을 변경하면 `CalculateDiscountService`의 코드도 함께 변경되어야 합니다.
즉, 인프라스트럭처에 의존하면
"테스트의 어려움"과 "기능 확장의 어려움"이라는 두 가지 문제가 발생하게 됩니다.
이를 해소하기 위해 DIP를 이용합니다.
DIP
고수준 모듈
의미 있는 단일 기능을 제공하는 모듈
- `CalculateDiscountService`는 "가격 할인 계산"이라는 기능을 구현합니다.
- 여러 하위 기능이 필요합니다.
저수준 모듈
하위 기능을 실제로 구현한 것
- 실제로 고객 정보를 구하는 것
- 룰을 실행하는 것
고수준 모듈이 동작하기 위해선 저수준 모듈을 사용해야 합니다. 하지만, 이 경우 위에서 언급한 구현 변경과 테스트가 어렵다는 문제점이 발생합니다.
이를 해결하기 위해 DIP는 저수준 모듈이 고수준 모듈에 의존하도록 바꿉니다.
이 과정은 추상화한 인터페이스를 통해 이루어 집니다.
`CalculateDiscountService`입장에서는 '고객 정보와 구매 정보에 룰을 적용해 할인 금액을 구한다' 라는 것만 중요할 뿐, 룰 적용을 어떤 것을 이용했는지는 중요하지 않습니다.
따라서, 룰에 대한 부분을 다음과 같이 추상화 할 수 있습니다.
public interface RuleDiscounter {
Money applyRules(Customer customer, List<OrderLine> orderLines);
}
`CalculateDiscountService`가 이를 이용하도록 바꿔봅시다.
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`가 룰을 적용한다는 사실만 알뿐입니다.
룰 적용을 구현한 클래스는 `RuleDiscounter` 인터페이스를 상속받아 구현합니다.
public class DroolsRuleEngine implements RuleDiscounter{
private KieContainer kieContainer;
public DroolsRuleEngine() {
KieServices kieServices = KieServices.Factory.get();
kieContainer = kieServices.getKieClasspathContainer();
}
@Override
public Money applyRules(Customer customer, List<OrderLine> orderLines) {
KieSession kieSession = kieContainer.newKieSession("discountSession");
try {
...
kieSession.fireAllRules();
} finally {
kieSession.dispose();
}
return money.toImmutableMoney();
}
}
결국 다음과 같은 구조로 코드가 바뀐 것을 알 수 있습니다.
- 더 이상 구현 기술인 `Drools`에 의존하지 않습니다.
- 추상화한 `RuleDiscounter` 인터페이스에 의존합니다.
- "룰을 이용한 할인 금액 계산"은 고수준 모듈의 개념이기 때문에 `RuleDiscounter`는 다음과 같이 고수준 모듈에 해당합니다.
- DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존하게 됩니다.
DIP 주의사항
DIP의 핵심은
고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함입니다.
단순히 아래와 같이 저수준 모듈에서 인터페이스만 추출하는 것은 잘못된 구조입니다.
DIP를 적용할 때
하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 합니다.
DIP와 아키텍처
결국 앞서 제시했던 계층 구조에서 DIP를 적용하면 다음과 같이 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 됩니다.
결국 인프라스트럭처에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스르 상속받아 구현하는 구조가 되어, 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능합니다.
도메인 영역의 주요 구성요소
도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현합니다.
엔티티 : ENTITY
- 고유의 식별자를 갖는 객체
- 자신의 라이프 사이클을 가집니다.
- 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공합니다.
- 주문, 회원 등과 같이 도메인의 고유한 개념을 표현합니다.
주의할 점은
실제 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 같은 것이 아닙니다.
도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점에서 차이가 있습니다.
밸류 : VALUE
- 고유의 식별자를 갖지 않는 객체
- 개념적으로 하나인 값을 표현
- 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용 가능
- 배송지 주소를 표현하는 주소(Address), 구매 금액을 위한 금액(Money) 등...
밸류 타입은 불변으로 구현할 것을 권장하며, 데이터를 변경할 때는 객체 자체를 완전히 교체해야 합니다.
애그리거트 : AGGREGATE
- 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
- 관련 객체를 하나로 묶은 군집
- 개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 큰 틀에서 도메인 모델을 관리할 수 있습니다.
- 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 "주문" 애그리거트로 묶을 수 있습니다.
루트 엔티티
군집에 속한 객체를 관리하는 엔티티
- 외부에서는 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근할 수 있습니다.
- 이를 통해 애그리거트 단위로 구현을 캡슐화할 수 있도록 합니다.
리포지터리 : REPOSITORY
- 도메인 모델의 영속성을 처리
- 구현을 위한 도메인 모델
- 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의
- 예를 들어, DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공
`OrderRepository`를 예시로 본다면, 도메인 객체를 영속화하는 데 필요한 기능을 추상화한 것이므로 고수준 모듈에 속합니다. 마찬가지로 기반 기술을 이용해 `OrderRepository`를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속합니다.
도메인 서비스 : DOMAIN SERVICE
- 특정 엔티티에 속하지 않은 도메인 로직을 제공
- 예를 들어, "할인 금액 계산"은 상품, 쿠폰, 회원 등 다양한 조건을 이용해 구현하게 됩니다. 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현합니다.
'DDD' 카테고리의 다른 글
[DDD] : 표현 영역 (0) | 2025.03.11 |
---|---|
[DDD] : 응용 영역 (0) | 2025.03.10 |
[DDD] : 리포지터리와 모델 구현 (0) | 2025.03.06 |
[DDD] : 도메인 모델 (0) | 2025.02.27 |