아키텍처
표현 영역
- 사용자의 요청을 받아 응용 영역에 전달하고 응용영역의 결과를 다시 사용자에게 전달한다.
- HTTP요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고 응용영역의 응답을 HTTP응답으로 변환하여 전송한다.
- Controller
응용 영역
- 시스템이 사용자에게 제공해야할 기능을 구현
- 기능 구현을 위해 도메인 모델을 사용하고 로직을 직접 수행하기 보다는 도메인 모델에 로직 수행을 위임한다.
- Service
도메인 영역
- 도메인의 핵심로직을 구현
- 기능 구현을 위해 도메인 모델을 사용하고 고직을 직접 수행하기 보다는 도메인 모델에 로직 수행을 위임한다.
- @Entity, @Repository
인프라스트럭처 영역
- 논리적인 영역보다는 실제 구현을 다루는 영역
- RDBMS, MONGO DB, SMTP, HTTP 클라이언트를 이용한 외부 API 호출
- 표현/응용/도메인 영역은 구현기술을 사용한 코드를 직접 만들지 않고 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
계층 구조 아키텍처
- 상위 계층에서 하위 계층으로의 의존만 존재한다.
- 응용 계층은 바로 아래 도메인 계층에만 의존해야하지만 구현의 편리함을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 한다.
[전형적인 계층 구조상의 의존 관계]
계층 구조에 따르면 도메인과 응용 계층은 DB나 외부시스템 연동을 위해 인프라스트럭처에 의존하게 된다.
// 인프라스트럭처 영역
public class DroolsRuleEngine {
private KContainer kcontainer;
...
//룰 엔진 구현
public void evaluate(String sessionName, List<?> facts) {
KSession kSession = kContainer.newKSession(sessionName);
... (구현 생략) ...
}
}
// 응용 영역이 인프라스트럭처 영역인 DroolsRuleEngine에 의존하고 있다.
public class CalculateDiscountService {
private DroolsRuleEngine ruleEngine;
public CalculateDiscountService() {
ruleEngine = new DroolsRuleEngine();
}
public Money calculatedDiscount(List<OrderLine> orderLines, String customerId) {
Customer customer = findCustomer(customerId);
MutableMoney money = new MutableMoney(0);
List<?> facts = Arrays.asList(customer, money);
facts.addAll(orderLines);
ruleEngine.evalueate("discountCalculation", facts); //discountCalculation 는 구현체의 세션이름으로 사용된다.
return money;
}
}
위 코드에는 아래와 같은 두가지 문제점이 존재한다.
1. 테스트가 어렵다.
CalculateDiscountService 가 올바르게 동작하는지 확인 하기 위해서는 Drools 코드가 잘 동작됨이 우선 보장되어야 한다.
2. 기능 확장이 어렵다.
내부에 Drools 코드에 특화된 코드가 많기 때문에 구현 방식을 변경하기 어렵다.
예를들어 Drools의 세션 이름을 변경하면 CalculateDiscountService 의 코드도 함께 변경해야 한다.
위 문제는 DIP를 통해 고수준 모듈이 더이상 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존하게 하여 해결할 수 있다.
DIP (Dependency Inversion Principle)
고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데,
반대로 저수준 모듈이 고수준 모듈에 의존할 때 DIP(의존역전원칙) 라고 부른다
고수준 모듈 : 의미 있는 기능을 제공하는 모듈
저수준 모듈 : 고수준 모듈을 구현하기 위해 필요한 하위 기능들을 실제로 구현한 것
[DIP 적용]
DIP 적용 시 장점
1. 변경이 유연해짐 - 구현 기술 교체가 용이
- CalculateDiscountService는 생성자 주입방식을 통해 RuleDiscounter 을 받는다.
- 구현 기술이 바뀌면 Service로직의 생성자 파라미터만 바뀐 구현체로 변경해주면 된다.
// 사용할 저수준 객체 생성
//RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
// 구현체가 바뀌면 바뀐 구현체 객체 생성
RuleDiscounter ruleDiscounter = new NewRuleDiscounter();
// 서비스 생성자 주입
CalculateDiscountService service = new CalculateDiscountService(ruleDiscounter);
2. 테스트가 쉬워진다.
- CalculateDiscountService 가 의존하는 객체의 동작여부에 상관 없이 테스트 코드 작성 가능
- 테스트 코드에서의 객체의 의존 주입은 mock객체 사용
[DIP 적용시 주의사항]
- DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다.
- DIP 목적은 고수준 모듈이 저수준 모듈에 의존하지 않게 하기 위해서임
- 저수준에서 인터페이스를 추출하는 것을 조심
[DIP와 아키텍처]
- DIP를 적용하면 아키텍처 수준에는 인프라스트럭처 영역이 응용영역과 도메인 영역에 의존하는 구조가 된다.
- 응용영역과 도메인 영역에 영향을 최소화 하면서 구현체를 변경하거나 추가 할 수 있다.
💡DIP를 항상 적용할 필요는 없다. 사용하는 구현 기술에 따라 완벽한 DIP를 적용하기보다는 구현 기술에 의존적인 코드를 도메인에 일부 포함하는게 효과적일 때도 있다.
💡예를 들어 프레임워크의 기술(@Transaction)이나 영속성 처리(@Entity)와 같은 경우에는 DIP가 주는 이점보다 구현의 편리함이 주는 이점이 더 크다
💡 추상화 할 부분이 잘 떠오르지 않으면 DIP 이점을 얻는 수준에서 적용 범위 고민해보자
도메인 영역의 주요 구성요소
엔티티 (ENTITY)
- 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 도메인의 고유한 개념을 표현하며 도메인 모델의 데이터와 관련된 기능을 제공한다. 예) Order, Member
- 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화 해서 데이터가 임의로 변경되는 것을 막는다.
- 도메인 모델의 엔티티와 DB관계형 모델의 엔티티는 다르다.
- 엔티티는 데이터 + 기능 포함
- 두개 이상의 (DB) 데이터가 밸류(VALUE)타입으로 엔티티에 포함 될 수 있다. 예) 주문자 이름 + 주소 = Orderer
밸류(VALUE)
- 개념적으로 표현할 때 사용되며 고유의 식별자가 없다. 엔티티의 속성으로도 사용되고 다른 밸류의 속성으로 사용될 수도 있다. 예) Address, Money(금액)
- 밸류 타입의 데이터를 변경시 새로운 객체로 교체한다. (불변)
애그리거트(AGGREGATE)
- 연관된 엔티티와 밸류 객체를 개념적으로 묶어놓은 군집.
- 예) '주문'애그리거트에는 Order엔티티와 Orderer라는 밸류가 포함되어 있다.
- 루트 엔티티를 갖는다.
💡루트엔티티는 애그리거트의 상태를 관리한다. 루트엔티티는 애그리거트에 속해있는 엔티티와 밸류 객체를 이용해 기능을 제공한다.
애그리거트를 사용하는 코드는 루트엔티티를 통해서 다른 엔티티나 객체에 접근 할 수 있다.
- 서비스가 크고 복잡해질수록 많은 엔티티와 밸류가 출현 하는데, 이 때 애그리거트는 도메인 모델을 상위 수준에서 바라볼 수 있게 해준다.
리포지터리(REPOSITORY)
- 구현을 위한 도메인 모델로 고수준 모듈에 속한다. (도메인 모델의 영속성을 처리하는데 필요한 기능을 추상화 했다.)
- 엔티티 객체를 조회하거나 저장하는 기능을 제공한다.
도메인서비스(DOMAIN SERVICE)
- 특정 엔티티에 속하지 않는, 여러 엔티티와 밸류를 필요로 하는 도메인 로직을 제공한다.
- 예) 할인금액 계산은 상품, 쿠폰, 회원, 주문금액 등의 다양한 조건이 필요한데 이때 도메인 서비스에서 로직을 구현한다.
요청처리 흐름
- Controller
- 사용자가 전송한 데이터 형식 validation 검사, 서비스가 요구하는 형식으로 변환
- 문제가 없다면 Service에 위 데이터와 함께 기능 실행 위임
- Service
- 도메인 모델을 이용해서 기능 구현
- 기능구현에 필요한 도메인 객체를 리포지토리에서 가져오거나, 신규 도메인 객체를 저장
- 도메인 상태를 변경하게 되는 영역이므로 트랜잭션 관리 필요
- Domain Object
- 서비스에서 요청한 도메인 로직 실행
- Repository
- DB로부터 도메인 객체 조회하거나 저장
모듈 구성
아키텍처의 각 영역은 별도 패키지에 위치하고, 패키지 구성에 정답은 없다.
- 도메인이 크다면 하위 도메인별로 모듈을 나눈다
- 도메인 모듈은 도메인에 속한 애그리커트를 기준으로 다시 패키지를 구성한다.
- 애그리거트, 모델, 리포지토리는 같은 패키지에 위치시킨다.
- 도메인이 복잡하면 도메인 모델과 도메인 서비스를 별도 패키지에 위치 시킬 수도 있다.
아키텍처별, 도메인별로 패키지 구분은 필요하다. 나만의 방식을 찾아 놓으면 유용할 듯
Reference
'객체지향' 카테고리의 다른 글
[DDD, 도메인 주도 개발] 5.스프링 데이터 JPA를 이용한 조회 기능 (0) | 2023.04.09 |
---|---|
[DDD, 도메인 주도 개발] 4.리포지터리와 모델 구현 (0) | 2023.03.28 |
[DDD, 도메인 주도 개발] 3.애그리거트와 애그리거트 루트 (0) | 2023.03.20 |
[DDD, 도메인 주도 개발] 1. 도메인 모델 시작하기 (0) | 2023.03.06 |
[OOP(Object-Oriented Programming)] 객체지향 5 원칙 SOLID (0) | 2021.04.06 |