도메인 주도 개발 시작하기(최범균 저) 책을 읽고 정리한 내용입니다.
DDD란 ??
Domain Driven Design은 도메인 패턴을 중심으로 설계하는 방식을 말한다. 아키텍처 설계를 도메인 모델과 도메인 로직에 초점을 맞추는 것이다. Ubiquitous language(보편적 언어)의 사용으로 도메인 전문가와 개발자 간의 커뮤니케이션 문제를 없애고 문서와 코드에서 동일한 표현과 단어로 구성된 언어를 사용하여 통일된 방식의 의사소통을 가능하게 해준다. 이번 글에서는 도메인 주도 설계를 위한 도메인 모델과 아키텍처에 대해 알아본다.
도메인 모델
도메인 모델은 특정 도메인을 개념적으로 표현한 것이며, 다수의 하위 도메인으로 구성되어 있다. 도메인은 Entity와 Value로 구분할 수 있다.
1
2
3
4
5
6
7
8
public class Order {
private OrderNo orderNo;
private Address address;
private Money money;
private Receiver receiver;
}
Entity는 고유의 식별자를 가지는 것이 특징이다. 테이블의 PK로 생각할 수 있다. Value는 개념적으로 완전한 하나를 표현할 때 사용한다. 위의 예시는 Order라는 entity와 이를 구성하는 value에 대한 것이다. 주문
이라는 entity는 OrderNo 타입을 가진 고유의 식별자를 갖는다. OrderNo는 value type으로 주문 번호
라는 개념적인 요소를 표현한다.
1
2
3
4
5
6
public class Receiver {
private String name;
private String phoneNumber;
}
Receiver 타입을 살펴보면 주문
에서 받는 사람
을 개념적으로 표현한 타입이다. Receiver 타입에는 name과 phoneNumber로 구성되어 있고 이는 받는 사람의 정보를 표현한다.
도메인 로직
1
2
3
4
5
6
7
8
9
10
public class Money { // value type에서 "돈 계산" 기능 제공
private int value;
public Money add(Money money) {
return Money.builder()
.value(this.getValue() + money.getValue())
.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Order { // entity에서 "주문 정보 변경" 기능 제공
private OrderNo orderNo;
private Address address;
private Money money;
private Receiver receiver;
public void changeShippingInfo(ShippingInfo shippingInfo) {
// action something
}
}
도메인 모델의 entity와 value type은 데이터를 가지고 있으면서 기능도 포함할 수 있다. 특정 entity에 속하지 않은 도메인 로직을 제공한다. 위 예시와 같이 value type을 위한 기능을 제공하거나 도메인 로직에서 여러 entity와 value가 요구된다면 도메인 서비스에서 로직을 구현할 수 있다.
DDD 아키텍처
도메인 주도 설계를 하기위한 아키텍처를 알아본다.
아키텍처를 구성하는 영역은 네 가지가 있다. 표현(presentation), 응용(application), 도메인, 인프라(infrastructure)로 구성된다.
표현 영역은 사용자의 요청을 처리하고 정보를 보여주는 영역이다. 소프트웨어를 사용하는 사람일 수도 있고, 외부 시스템일 수도 있다.
응용 영역은 사용자가 요청한 기능을 실행하는 역할을 한다. 비즈니스 로직을 직접 구현하지 않지만, 도메인 계층을 조합해서 기능을 실행한다.
도메인 영역은 서비스가 제공할 도메인 규칙을 구현하고, 인프라 영역은 DB 접근, 외부 api 호출 등 서비스 외부 시스템과 연동을 처리하는 부분이다.
DIP
의존 역전 원칙(Dependency Inversion Principle)은 객체 지향 5대 원칙 중 하나로 저수준 모듈이 고수준 모듈에 의존하도록 설계하는 것이다.
1
2
3
public interface JoinByRule {
Member applyJoinRule(MemberInfo memberInfo, MemberClassification memberClassification);
}
1
2
3
4
5
6
7
8
public class MemberJoinByRule implements JoinByRule {
@Override
public Member applyJoinRule(MemberInfo memberInfo, MemberClassification memberClassification) {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RequiredArgsConstructor
public class MemberJoinService {
private final JoinByRule joinByRule;
public void memberJoinByClassification(MemberInfo memberInfo,
MemberClassification memberClassification) {
Member member = joinByRule.applyJoinRule(memberInfo, memberClassification);
}
}
MemberJoinService는 회원 가입을 수행하는 클래스이며 회원의 분류에 따라 저장되는 회원 정보가 다르게 저장된다. 여기서 JoinByRule에서 회원 분류에 따른 저장 정보를 반환하게 되는데 인터페이스를 사용해 회원 분류에 따른 정보를 얻는다. MemberJoinService는 JoinByRule 내부 구현체가 어떻게 구현되어 있는지 알 필요가 없다. MemberJoinService는 JoinByRule 인터페이스에 의존한다. MemberJoinByRule는 고수준의 하위 기능인 JoinByRule를 구현한 것으로 저수준 모듈에 속한다. DIP를 만족한 설계로 구현체애 의존하지 않아 구현 변경에 용이하고, 테스트가 쉽도록 만들 수 있다.
도메인 영역의 주요 구성 요소
도메인 영역의 구성 요소로 entity, value, aggregate, repository가 있다. 도메인을 기준으로 데이터를 조회하고 변경하는 아키텍처의 구성을 알아본다.
Aggregate
애그리거트는 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 주문과 관련된 Order 엔티티, OrderLine 밸류, Order 밸류 객체를 ‘주문’ 애그리거트로 묶을 수 있다. Order는 애그리거트의 루트이고 속한 객체들을 관리한다. 주문 애그리거트는 Order를 통해서 정보 변경이 가능하고, 따라서 Order가 구현한 도메인 로직을 항상 따른다.
Repository
1
2
3
4
5
6
7
public interface OrderRepository {
Order findByNumber(OrderNo orderNo);
void save(Order order);
void delete(Order order);
}
레포지토리는 구현을 위한 도메인 모델이다. 애그리거트 단위로 데이터를 변경하거나 조회하는 기능을 정의한다. 레포지토리가 데이터를 변경하고 조회하는 기준은 애그리거트의 루트이다. 레포지토리는 도메인 객체를 영속화하기 위해 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다. 추상화된 레포지토리의 구현체는 저수준 모듈로 인프라 영역에 속한다. 이미지와 같이 OrderRepository는 도메인 모델 영역에 속하고 구현체는 인프라 영역에 속하게 된다. 레포지토리를 소비하는 주체는 응용 계층이다.
Infrastructure
인프라 영역은 도메인 객체의 영속성 처리, 트랜잭션, rest client 등 다른 영역에서 필요로하는 구현 기술을 지원한다. 인프라 영역은 외부 시스템에 직접 접근하는 부분으로 kafka, RMQ 등 논리적 개념보다 실제 구현을 다룬다.
정리
도메인 주도 개발은 도메인을 중심의 아키텍처로 유연하고 확장성 있는 서비스를 설계할 수 있다고 생각한다. 도메인 모델에 대한 로직은 도메인 객체 안에서 이뤄지고, 의존 역전 원칙을 따르는 클래스 설계로 낮은 결합도를 가진 서비스 설계가 가능하다고 생각한다.
이미 만들어진 서비스에 적용하긴 힘들것 같다고 생각한다. 레거시 코드는 단순한 계층형 구조로 설계되어 있고, 보편적 언어를 사용하지 않기 때문에 도메인 중심의 설계로 바로 변경하긴 어려울 수 있다. 하지만, 최소한의 도메인 중심 설계를 통해 확장성 높은 서비스로 리팩토링이 가능할 수 있을 것 같다.
도메인 주도 개발과 port & adapter 패턴을 결합해보는 것을 목표로 글을 정리할 예정이다.