본문은 [도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지]를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
2.1 네 개의 영역
- 표현, 응용, 도메인, 인프라스트럭처는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역이다
- 표현 영역
- 사용자의 요청을 받아 응용 영역에 전달하고, 응용 내역의 처리 결과를 다시 사용자에게 보여주는 역할(대표적으로 스프링 MVC 프레임워크가 여기에 해당)
- 응용 영역
- 시스템이 사용자에게 제공해야 할 기능을 구현한다(주문 등록, 주문 취소, 상품 상세 조회 등)
- 응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다([그림2.2] 참고)
- 도메인 영역
- 도메인 모델을 구현한다(1장의 Order, OrderLine, ShippingInfo 등)
- 도메인 모델은 도메인의 핵심 로직을 구현한다
- 인프라스트럭처 영역
- 구현 기술에 대한 것을 다룬다
- RDBMS 연동 처리
- 메시징 큐에 메시지를 전송하거나 수신하는 기능 구현
- 몽고DB나 레디스와의 데이터 연동을 처리
- SMTP를 이용한 메일 발송 기능 구현
- HTTP 클라이언트를 이용해서 REST API를 호출하는 것을 처리
- 논리적인 개념을 표현하기보다는 실제 구현을 다룬다
- 구현 기술에 대한 것을 다룬다
- 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않고, 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다
- DB의 데이터가 필요하면 인프라스트럭처 영역의 DB 모듈을 사용하여 데이터를 읽어온다
- 외부에 메일을 발송해야 한다면 인프라스트럭처 영역의 SMTP 연동 모듈을 이용해서 메일을 발송한다
2.2 계층 구조 아키텍처
- 네 영역을 구성할 때는 [그림 2.4]와 같은 계층 구조의 아키텍처를 많이 사용한다
- 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다
- 계층 구조를 엄격하게 적용한다면 상위 계층은 바로 아래의 계층에만 의존을 가져야 하지만 구현의 편리함을 위해 계층 구조를 유연하게 적용하기도 한다
- 예를 들어 응용 계층은 바로 아래의 도메인 계층에 의존하지만, 외부 시스템과의 연동을 위해 더 아래 계층인 인프라스트럭처 계층에 의존하기도 한다
- 이러한 상황에서 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속되는 문제가 발생한다
- 인프라스트럭처에 의존하면 '테스트 어려움'과 '기능 확장의 어려움'이라는 두 가지 문제가 발생하게 된다
- 이 두 문제는 DIP를 통해서 해결될 수 있다
2.3 의존 역전 원칙(DIP)
- 가격 할인 계산을 하기 위해선 [그림 2.7]의 왼쪽과 같이 고객 정보를 구해야 하고, 구한 고객 정보와 주문 정보를 이용해서 룰을 실행해야 한다
- 여기서 CalculateDiscountService는 의미 있는 단일 기능을 제공하는 고수준 모듈이다
- 고수준 모듈의 기능을 구현하려면 여러 하위 기능이 필요하다
- 그러나 고수준 모듈이 저수준 모듈을 사용하면 앞서 계층 구조 아키텍처에서 언급했던 두 가지 문제, 즉 구현 변경과 테스트가 어렵다는 문제가 발생한다
- DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾸며, 이는 추상화한 인터페이스를 통해 이루어진다
- 먼저 고수준 모듈의 개념에서 필요한 수준으로 인터페이스를 추상화한다
- 고수준 모듈은 이 인터페이스를 의존하고, 저수준 모듈은 이 인터페이스를 구현한다
- 실제 사용할 저수준 구현 객체는 의존 주입을 통해서 전달 받는다
- DIP를 적용하면 [그림2.9]와 같이 저수준 모듈이 고수준 모듈에 의존하게 된다 (그림에선 저수준과 고수준 글씨가 반대로 적혀있다)
- 저수준 모듈이 고수준 모듈에 의존한다고 해서 DIP(Dependency Inversion Principle), 의존역전 원칙이라고 부른다
- 의존역전 원칙을 통해 구현 변경의 어려움을 해결할 수 있다
- (컴파일 타임에) 고수준 모듈은 실제 구현체가 어떤 것인지 모른 채 주입받는다
- 스프링과 같은 의존 주입을 지원하는 프레임워크를 사용하면 쉽게 구현체를 변경할 수 있다
// 1. 사용할 저수준 객체 생성 (RuleDiscounter는 인터페이스)
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();
// 2. 생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDisocuntService(ruleDiscounter);
// 3. 사용할 저수준 구현 객체 변경
RuleDiscounter ruleDiscounter = new SimpleRuleDiscounter();
// 4. 사용할 저수준 모듈을 변경해도 고수준 모듈을 수정할 필요가 없음
CalculateDiscountService disService = new CalculateDisocuntService(ruleDiscounter);
- 테스트의 어려움 또한 해결할 수 있다
- CustomerRepository와 RuleDicounter 등의 고수준의 인터페이스를 사용하면 대역 객체를 사용해서 테스트를 진행할 수 있다
- 대역에 관한 내용은 여기를 참고하자
DIP 주의사항
- DIP를 잘못 사용하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다
- DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만 보고 [그림2.10]과 같이 저수준 모듈에서 인터페이스를 추출하는 경우가 있다
- DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다
- CalculateDiscountService 입장에서 봤을 때 할인 금액을 구하기 위해 룰 엔진을 사용하는지 직접 연산하는지는 중요하지 않다
- 단지 규칙에 따라 할인 금액을 계산한다는 것이 중요하다
- 따라서 '할인 금액 계산'을 추상화한 인터페이스는 고수준 모듈에 위치한다
- 또한, DIP를 항상 적용할 필요는 없다
- 사용하는 기술에 따라 완벽한 DIP를 적용하기보다는 구현 기술에 의존적인 코드를 도메인에 일부 포함하는 게 효과적일 때도 있다
- 또는 추상화 대상이 잘 떠오르지 않을 때도 있다
- 이럴 때는 무조건 DIP를 적용하려고 시도하지 말고 DIP의 이점을 얻는 수준에서 적용 범위를 검토해보자
DIP와 아키텍처
- 인프라스트럭처 계층이 가장 하단에 위치하는 계층형 구조와 달리 아키텍처에 DIP를 적용하면 [그림2.12]와 같이 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존(상속)하는 구조가 된다
- 인프라스트럭처에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능하다
2.4 도메인 영역의 주요 구성요소
- 도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현한다
1) 엔티티와 밸류
- 엔티티와 밸류는 이미 1장에서 언급한 바가 있다
- 도메인 모델의 엔티티와 DB 모델의 엔티티는 다르다
- 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다
- 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터구조라기보다는 데이터와 함께 기능을 제공하는 객체이다
- 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다
- 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다
- RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘들다
- 1장에서 설명한 것처럼 밸류는 불변으로 구현할 것을 권장하며, 이는 엔티티의 밸류 타입 데이터를 변경할 때는 객체 자체를 완전히 교체한다는 것을 의미한다
2) 애그리거트
- 도메인이 커질수록 개발한 도메인 모델도 커지면서 많은 엔티티와 밸류가 출현하며, 엔티티와 밸류 개수가 많아질수록 모델은 점점 더 복잡해진다
- 도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하는 상황이 발생한다
- 이때 상위 수준에서 모델을 관리하지 않고 개별 요소에만 초점을 맞추다 보면 큰 수준에서 모델을 이해하지 못해 큰 틀에서 모델을 관리할 수 없는 상황에 빠질 수 있다
- 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트(AGGREGATE)이다
- 애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 된다
- 개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현하게 되며, 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있다
- 애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다
- 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다
- 애그리거트를 사용하는 코드는 애그리거트 루트가 제공하는 기능을 실행하고, 애그리거트 루트를 통해서 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다
- 이는 애그리거트의 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다
- 애그리거트를 구현할 때는 고려할 것이 많은데, 이는 3장에서 살펴보도록 한다
3) 리포지터리
- 도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL, 로컬 파일과 같은 물리적인 저장소에 도메인 객체를 보관해야 한다
- 이를 위한 도메인 모델이 리포지터리(Repository)이다
- 엔티티나 밸류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델이다
- 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다
- [그림 2.19]의 전체 모듈 구조를 살펴보자
- 도메인 모델 관점에서 OrderRepository는 도메인 객체를 영속화하는 데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다
- 기반 기술을 이용해서 OrderRepository를 구현한 클래스는 저수준 모듈로 인프라스트럭처 영역에 속한다
- 응용 서비스와 리포지터리는 밀접한 연관이 있다
- 응용 서비스는 필요한 도메인 객체를 구하거나 저장할 때 리포지터리를 사용한다
- 응용 서비스는 트랜잭션을 관리하는데, 트랜잭션 처리는 리포지터리 구현 기술의 영향을 받는다
- 리포지터리를 사용하는 주체가 응용 서비스이므로 리포지터리는 응용 서비스가 필요로 하는 메서드를 제공하는데, 다음 두 메서드가 기본이 된다(필요에 따라 다른 메서드를 추가적으로 제공한다)
- 1) 애그리거트를 저장하는 메서드
- 2) 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드
- 리포지터리를 구현하는 방법은 선택한 구현 기술에 따라 달라진다(이는 4장에서 살펴볼 예정이다)
2.5 요청 처리 흐름
- 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다
- 이때 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다
- 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다
2.6 인프라스트럭처 개요
- 인프라스트럭처는 표현 영역, 응용 영역, 도메인 영역을 지원한다
- 도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다
- DIP에서 언급한 것처럼 도메인 영역과 응용 영역에서 인프라스트럭처의 기능을 직접 사용하는 것보다는 이 두 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다
- 하지만 무조건 인프라스트럭처에 대한 의존을 없앨 필요는 없다
- 예를 들어 스프링을 사용할 경우 응용 서비스는 트랜잭션 처리를 위해 스프링이 제공하는 @Transactional을 사용하는 것이 편리하다
- 영속성 처리를 위해 JPA를 사용할 경우 @Entity나 @Table과 같은 JPA 애너테이션을 도메인 모델 클래스에 사용하는 것이 XML 매핑 설정을 이용하는 것보다 편리하다
- 구현의 편리함은 DIP가 주는 다른 장점(변경의 유연함, 테스트가 쉬움)만큼 중요하므로 DIP의 장점을 해치지 않는 선에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 나쁘지 않다
- 인프라스트럭처에 대한 의존을 완전히 갖지 않도록 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다
2.7 모듈 구성
- 아키텍처의 각 영역은 별도 패키지에 위치한다
- 패키지 구성 규칙에 정답이 존재하는 것은 아니지만 [그림 2.21]과 같이 영역별로 모듈이 위치할 패키지를 구성할 수 있을 것이다
- 도메인이 크면 [그림2.22]와 같이 하위 도메인으로 나누고 각 하위 도메인마다 별도 패키지를 구성한다
- 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다
- 예를 들어 카탈로그 하위 도메인이 상품 애그리거트와 카테고리 애그리거트로 구성될 경우 [그림 2.23]과 같이 도메인을 두 개의 하위 패키지로 구성할 수 있다
- 애그리거트, 모델, 리포지터리는 같은 패키지에 위치시킨다
- 예를 들어 주문과 관련된 Order, OrderLine, Orderer, OrderRepository 등은 com.myshop.order.domain 패키지에 위치시킨다
- 도메인이 복잡하면 도메인 모델과 도메인 서비스를 다음과 같이 별도 패키지에 위치시킬 수도 있다
- com.myshop.order.domain.order: 애그리거트 위치
- com.myshop.order.domain.service: 도메인 서비스 위치
- 응용 서비스도 다음과 같이 도메인 별로 패키지를 구분할 수 있다
- com.myshop.catalog.domain.product
- com.myshop.catalog.domain.category
- 모듈 구조를 얼마나 세분화해야 하는지에 대해 정해진 규칙은 없다 한 패키지에 너무 많은 타입이 몰려서 코드를 찾을 때 불편한 정도만 아니면 된다
- (필자의 의견) 한 패키지에 가능하면 10~15개 미만으로 타입 개수를 유지하려고 노력하되, 이 개수가 넘어가면 패키지를 분리하는 시도를 해본다
'책 > 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 6장: 응용 서비스와 표현 영역 (0) | 2022.10.14 |
---|---|
[도메인 주도 개발 시작하기] 5장: 스프링 데이터 JPA를 이용한 조회 기능 (0) | 2022.10.07 |
[도메인 주도 개발 시작하기] 4장: 리포지터리와 모델 구현 (0) | 2022.10.03 |
[도메인 주도 개발 시작하기] 3장: 애그리거트 (1) | 2022.09.20 |
[도메인 주도 개발 시작하기] 1장: 도메인 주도 개발 시작하기 (0) | 2022.09.07 |