[DDD] 도메인 주도 개발 - (1) 좋은 아키텍처와 도메인 주도 설계 [DDD] 도메인 주도 개발 - (2) 애그리거트 [DDD] 도메인 주도 개발 - (3) CQRS - 명령 중심 도메인 구현 [DDD] 도메인 주도 개발 - (4) CQRS - 조회 중심 도메인 구현 [DDD] 도메인 주도 개발 - (5) 도메인 서비스 [DDD] 도메인 주도 개발 - (6) 응용 서비스와 표현 영역 [DDD] 도메인 주도 개발 - (7) 애그리거트 트랜잭션 관리 [DDD] 도메인 주도 개발 - (8) 도메인 모델과 BOUNDED CONTEXT [DDD] 도메인 주도 개발 - (9) 이벤트 |
Contents
최근 프로젝트에 상사가 DDD(Domain driven design)을 적용했는데, 한 번 내용정리를 해보면 좋겠다 싶었다.
1. Domain 도메인이란?
도메인이란?
도메인은 영단어로 많은 뜻을 가지고 있는데, 영역, 범위라는 말로 한정지어서 생각해볼 때,
이때의 도메인은 문제영역이라고 볼 수 있다.
만약 신규 프로젝트로 쇼핑몰을 만들어야한다면, 쇼핑몰이 지금의 문제영역이 된다.
하위 도메인
도메인은 그 아래 하위 도메인을 가지는데,
쇼핑몰을 만들 때는 상품, 회원, 주문, 결제 등 다양한 또 다른 문제영역이 존재한다.
이 각각이 하위 도메인이 될 수 있고, 하위 도메인이 없을 때도 있다.
2. 도메인 주도 설계
도메인을 주도로 설계(Domain Driven Design)하려는 이유가 뭘까?
소프트웨어를 개발하고 유지보수 할 때는 '문제영역'을 아는 것이 중요하다.
빠르게 문제를 파악하고 요구사항을 반영해야하는데,
기존처럼 응용단에서 모든 비즈니스 로직을 처리해버리면
어떤 기능이 어떤 도메인에 대한 것인지 파악하기도 어렵고, 기능 중점으로 개발이 어려워진다 .
따라서, 도메인 주도 설계(Domain Driven Design)는 좋은 아키텍처를 전략적으로 만들기 위해,
도메인 패턴을 중심에 두고 설계, 구현하는 방식이다.
내 경우도 오랫동안 Domain을 단순히 table 맵핑 객체로만 생각하는 오류를 범하기도 했다.
(DB 테이블 엔티티와 도메인 엔티티를 구분하지 못한 것)
3. 도메인 모델
도메인 주도 설계를 하려면 도메인(문제영역)을 파악하고 이를 토대로 모델을 만들어야 한다.
즉, 도메인 모델은 특정 도메인을 개념적으로 표현한 개념 모델로, 도메인 이해를 위한 모델이다.
개념 모델에 모든 표현을 담을 수는 없으므로, 핵심 규칙을 담는 것을 목표로 한다.
도메인 모델 표현
도메인 모델은 UML, 그래프, 수학공식등 다양하게 표현이 가능하다.
1) 상태 다이어그램 (UML) : 클래스, 상태 다이어그램
2) 그래프
3) 수학 공식
도메인 모델 패턴
도메인 모델은 주로 개념모델로, DB, 트랜잭션, 성능, 구현 기술 등 현재 기술은 고려하지 않는다.
따라서 이를 고려한 구체적인 구현 모델이 필요한데,
도메일 모델 패턴은 아키텍처 상의 도메인 계층을 객체지향 기법으로 구현하는 패턴이다.
(마틴 파울러의 [엔터프라이즈 어플리케이션 아키텍처 패턴])
도메인 계층은 도메인 핵심규칙을 구현해야한다. 즉, 기능을 해당 도메인에 집중시키는 방식이다.
방식은 도메인 모델을 도출하면서 같이 구현해보겠다.
도메인 모델 도출
1. 요구사항분석
도메인 모델 도출은 요구사항 분석에서부터 시작된다.
만약, 쇼핑몰 회원 정보를 관리할 때 아래 내용을 분석해낼 수 있다.
1) 회원 가입
2) 회원 주소 변경
3) 회원 정보는 이름, 이메일, 주소, 전화번호가 기본
4) 주소는 '우편번호, 도시, 상세주소'로 구성
5) 회원의 현재상태를 표시 (활성, 일시 비활성, 탈퇴)
6) 회원은 식별 가능한 유일값이 있어야 한다.
1,2)는 기능에 대한 부분이고, 3,4,5)는 데이터 구성에 대한 부분이다.
이를 토대로 도메인을 도출하면 아래와 같다.
public class User {
private Long id;
private String name;
private String email;
private Address address;
private int phone;
private UserState userState;
// 기능을 응용 서비스가 아닌 도메인에서 가짐
public void signUp(User newUser){
checkAlreadyExisted(newUser); // 신규 가입 가능 여부 확인
...
}
public void changeAddress(Address newAddress){
checkAddressChangeable(); // 변경 가능 여부 확인
setAddress(newAddress); // 주소 변경
}
private void setAddress(Address newAddress){...};
}
기존에는 기능1,2를 응용서비스 단에서 구현했는데, 기능을 도메인으로 이전하는 것이 도메인 모델 패턴이다.
이렇게 되면
(1) 어떤 도메인(문제)에 해당하는 기능인지 명확히 알 수 있고,
(2) 변경 시, 해당 도메인만 수정하면 되니 응집도가 높아진다.
public class Address {
private int zipcode;
private String city;
private String detail;
}
4)번의 요구사항을 맞추기 위해, 주소의 세부적인 여러 값을 조합하여 하나의 개념을 나타냈다.
이를 Value라고 한다. (아래 더 자세히)
public enum UserState {
ACTIVATED, DEACTIVATED, WITHDRAWN
}
최근 개인정보 보호가 중요하므로, 5) 회원 상태를 나타낼 때, 열거타입(Enum)을 이용할 수 있다.
해당 도메인 용어는 내용을 알 수 있게 명확하게 적는다.
public class User {
private Long id;
...
}
또한 회원 이름은 중복될 수 있으니, id를 자동 생성되는 6) 유일값(DB Autoincrement)으로 삼았다.
2. Entity, Value 구분
도출된 도메인 모델은 Entity(엔티티), Value(밸류)로 크게 구분 된다.
위에서 도출한 User 도메인 모델을 보면 User가 엔티티, 그 안의 Address가 밸류가 된다.
엔티티와 밸류를 구분하는 것이 중요한데, 가장 큰 차이점은
Entity가 고유식별자를 가진다는 점이고,
Value는 여러 값의 조합으로 하나의 개념을 완성한다는 점이다.
Entity와 Value는 아래서 좀 더 살펴본다.
4. 아키텍처
도메인 모델까지 도출을 해냈다면, 이 도메인 모델이 시스템 어떤 영역에서 위치하고 쓰이는 지를 봐야한다.
당연히 도메인 계층에서 쓰이는데, 어떤 식으로 아키텍처가 작용하는지를 봐야한다.
어플리케이션 아키텍처는 주로 표현, 응용, 도메인, 인프라스트럭처, 4개의 계층구조를 가진다.
클라이언트(UI 등)에서 시스템으로 정보(파라미터, 쿠키, 헤더, url)를 요청하면
표현영역에서 그 정보를 판단하여 결과값을 전달한다.
기능 정의 | 예) | 계층 |
|
표현 계층 | - 사용자 요청을 해석, 판단하여, 원하는 기능을 제공 - 요청된 정보 수신 -> 정보 판단 -> 해당 기능의 서비스 실행 -> 결과값 전달 - 요청 정보 형식을 서비스에 맞게 변환 (결과값도 맞는 형식으로 변환) |
컨트롤러 | 상위 |
응용 계층 | - 실제 사용자가 원하는 기능을 제공 - 도메인 모델을 사용 - 로직을 직접 수행하지 않고, 도메일 모델에 위임 (직접 get, set을 하지 않고 도메인 메소드를 사용) |
서비스 | |
도메인 계층 |
- 도메인 핵심 모델을 구현 - Entity, Value, Aggregate, Repository, Domain Service로 구성 |
||
인프라 스트럭쳐 (인프라 구성요소) |
- 구현 레벨 (RDBS 연동, 큐 송수신 등) - 응용, 도메인 영역이 인프라스트럭처에 종속 |
하위 |
- 응용, 도메인 영역은 의미있는 단일 기능을 제공하는 고수준 모듈이고,
인프라스트럭처는 고수준 모듈에 필요한 하위 기능을 실제로 구현한 저수준 모듈이다.
계층 의존 관계
- 상위는 하위에 의존, 하위는 상위에 의존x
즉, 상위 계층은 직접 구현하지 않고, 인프라스트럭처가 제공하는 기술을 사용해서, 필요기능을 개발한다.
- 상위 계층이 하위 계층에 의존하기 때문에
1) 테스트가 어려움 (전부 구현이 끝나기 전에는)
2) 기능 확장이 어려움 (구현 기술 교체가 어려움)
는 단점을 가진다.
그 해결책이 DI이다.
DI (Dependency Inversion)
이를 DIP(Dependency Inversion Principle, 의존 역전 원칙)라고 하는데 스프링에서 자주 말하는 DI이다.
즉, 영향을 죄소화해서 구현기술을 변경하는 것으로
저수준 모듈(인프라스트럭처)이 추상화된 인터페이스를 이용해 고수준 모듈에 의존하도록 하는 것이다.
원래 상위가 하위계층에 의존하는 구조를 역전(Inversion)시킨 것이다.
이렇게 하면 1) 테스트 어려움, 2) 기능확장 어려움 두 가지 단점을 해결할 수 있는데
1) 테스트
인터페이스를 사용하므로, 대용객체 (테스트에 필요한 기능만) 사용해서 테스트를 할 수 있다.
주로 스프링에서는 Mockito 프레임워크를 많이 쓴다.
2) 기능 확장 (구현 기술 교체)
인터페이스를 사용하므로 코드 수정 없이,
저수준 구현 객체의 생성부만 생성자로 주입해주면 된다.
스프링은 의존 주입을 지원하니 원하는 시점에 원하는 기술을 주입할 수 있다.
이 때
1) 단순히 인터페이스와 구현부를 분리하는 게 아니고
2) 고수준 모듈이 저수준 모듈에 의존하면 안된다.
즉, 하위 기능을 추상화 해서 고수쥰 모듈에 넣으면 진정한 인터페이스로 보기가 어렵다.
고수쥰 모듈의 입장에서 필요한 기능에 대한 정의만 있으면 된다.
예) DB를 연결한다 / 전체 금액을 구한다...
좋은 아키텍처란?
결론적으로, 좋은 아키텍처는 계층 의존 관계를 해결 하기 위해
응용, 도메인 영역에 인터페이스를 만들어 인프라 스트럭처에서 상속받아 구현하는 구조이다.
예를 들어, 구현기술인 Mybatis를 JPA로 변경한다고 할 때,
응용 영역에 interface를 추가하여, 이를 상속받은 구현부(인프라 스트럭처)만 변경하면 간단하다.
5. 도메인 영역
도메인 영역은 드디어 도메인의 핵심 규칙을 구현하는 영역으로, 앞으로 다루고 배워야할 영역이다.
용어 | 정의 |
Entity (엔티티) | - 도메인의 고유한 개념 표현 - 고유 식별자O, 데이터+기능 (예) 주문(Order), 사용자(User) - 자신의 라이프 사이클을 가짐 |
Value | - 고유 식별자x, - 도메인 객체의 속성을 표현 (예) 사용자(User)의 나이(Age), 이름(Name), 주소(Address) - Entity 뿐만 아니라 다른 Value의 하부 속성으로도 가능 예) 주소(Address)는 우편번호(zipcode), 주소1, 주소2로 조합해서 구성 |
Aggregate (애그리거트) |
- 관련 엔티티와 밸류 객체를 개념적으로 묶은 것 예) 사용자 agreegate : 사용자 Entity, 주소 Value 등 |
Repository | - Aggregate의 저장소 - 도메일 모델의 영속성(객체 조회, 저장, 수정)을 처리 |
Domain Service (도메인 서비스) |
- 특정 Entity에 속하지 않고 여러 Entity를 필요로 할 때, 도메인 로직을 제공. |
* 영속성(Persistence) : 데이터를 생성한 프로그램이 종료되어도, 데이터가 사라지지 않음.
Entity
엔티티는 DB 모델(테이블) Entity와 도메인 모델 Entity, 2가지가 있는데
여기서 말하는 Entity는 도메인 모델 엔티티을 말한다.
기존에는 둘을 동일하게 보고 개발을 했는데, 가장 큰 차이점은 도메인 모델 엔티티가 기능을 가진다는 것이다.
cf) DB 모델 엔티티
- RDBMS는 value 타입 표현이 어렵다. 예를 들어
주문(Order) 테이블에 주문자 정보(이름, 이메일) 를 함께 넣으면 -> 주문자 정보가 강조X
주문(Order)와 별도의 테이블(Orderer)로 분리하여 주문자 정보만 저장하면 -> DB 모델 엔티티가 된다. (식별자 O)
특징 1) 기능 집중
- DB 테이블 엔티티 : 데이터
도메인 모델 엔티티 : 데이터 + 기능
public class User {
private Long id;
private String name;
private String email;
private Address address;
// 기능을 응용 서비스가 아닌 도메인에서 가짐
public void changeAddress(Address newAddress){
checkAddressChangeable(); // 변경 가능 여부 확인
setAddress(newAddress); // 주소 변경
}
private void setAddress(Address newAddress){...};
}
- 기존에는 응용 서비스에 changeEmail(이메일 변경기능)이 있었다면, 이제는 엔티티로 기능을 집중시켜주는 형태이다.
- 기능 구현을 캡슐화해서 데이터의 임의 변경을 막을 수 있다.
- setter는 응용, 표현 영역으로 로직이 분산될 수 있으므로 사용을 자제한다. (유지보수 어려움)
- 기능 구현 시 메소드 명은 set~ 보다는 change~, complete~ 등으로 명확하게 나타낸다.
- set은 단순 값 설정처럼 보이고, set을 하더라도 완전한 상태가 아닐 수 있다.
또 모든 필드를 각각 set을 하는 것 보다, 생성자를 이용해서 값을 셋팅하는게 좋다.
특징 2) 식별자를 가짐
public class User {
private Long id;
private String name;
private String email;
private Address address;
@Overrice
public boolean equals(Object obj){...}
@Overrice
public int hashCode(){...}
}
- 엔티티는 고유 식별자를 가지므로 고유한 값임을 보장할 수 있다.
고유한 값이므로 식별자가 같으면 euals(), hashCode()를 구현할 수 있다.
(참고) HashCode() : https://brunch.co.kr/@mystoryg/133
자바 hashCode()
자바의 hashCode() hashCode()는 객체의 hashCode를 리턴한다. hashCode는 일반적으로 각 객체의 주소값을 변환하여 생성한 객체의 고유한 정수값이다. 따라서 두 객체가 동일 객체인지 비교할 때 사용할
brunch.co.kr
- 식별자는 특정규칙, UUID, 직접 입력, 일련번호(시퀀스, Auto increment)로 생성할 수 있다.
DB의 Auto increment로 만든 자동증가값은 값이 DB에 들어가기 전에는 알수가 없다. 처리가 끝나고 반환값을 통해 비로소 식별자 값이 정해진다.
특징 3) Value를 가진다.
public class Address {
private int zipcode;
private String city;
private String detail;
}
Value
위의 Address처럼 Entity는 Value를 가지는데,
- 여러 데이터를 조합하여 하나의 개념으로 사용할 때 Value 타입을 사용한다.
꼭 여러 값일 필요는 없는데, 자료형(int, String)보다 전용 타입화 했을 때 특정 의미를 표현 할 수 있어서 더 명확하다
- Value는 User의 changeAddress(Address new Address)처럼 new Address 불변으로 안전하게 구현하는 것이 좋다.
(외부에서 상태변경 x)
- Value 타입을 쓰면, Value 전용 도메인 기능도 추가할 수 있다.
- Entity 안에 Value 타입이 여러 개라면, equals()를 구현할 때, Value들이 다 같은 객체인지 확인하도록 구현해야한다.
public class Address {
private int zipcode;
private String city;
private String detail;
public boolean equals(Object obj){
...
Address addr = (Address) obj;
return this.city.equals(addr.city) &&
this.detail.equals(addr.detail)
}
}
Aggregate
애그리거트는 연관된 Entity(e)와 Value(v)의 개념적 묶음으로 보면 되는데,
'주문정보(e), 회원정보(e), 주문자정보(v), 회원 주소(v), 주문 상세정보(v), 상품정보(e), 상품 이미지(v)' 등 여러 객체들이 혼재할 때, 이를 관련된 것들끼리 묶는 것이다.
회원 Aggregate ; 회원정보(e), 회원 주소(v)
주문 Aggregate : 주문정보(e), 주문자정보(v), 주문 상세정보(v)
상품 Aggregate : 상품정보(e), 상품이미지(v)
등으로 나눌 수 있다.
이를 기반으로 개별 객체간 관계가 아닌, 애그리거트의 관계로 도메인을 이해하고 구현해야한다.
Aggregate root
이 때 가장 중심기능이 되는 것이 Aggregate root인데, 주문 정보나 회원 정보가 애그리거트 루트가 될 수 있다
- 애그리거트 루트를 중심으로 기능을 실행하고, 다른 엔티티나 밸류에 접근한다.
- 애그리거트 내부에 숨겨져 있어서 캡슐화할 수 있다.
public class User {
private String name;
private string email;
private Address address;
// 기능을 응용 서비스가 아닌 도메인에서 가짐
public void changeAddress(Address newAddress){
checkAddressChangeable(); // 변경 가능 여부 확인
setAddress(newAddress); // 주소 변경
}
private void setAddress(Address newAddress){...};
}
소스에서 보다시피 주소를 변경하려면 응용단에 기능구현을 하지 않기 때문에,
User를 통하지 않고는 Address를 변경할 수가 없다.
cf) Context bounded
개인적으로 Aggregate와 Context bounded와 헷갈렸는데
context bounded는 같은 모델을 쓰느냐 아니냐로 갈린다고 보면 간단하다.
그래서 context bounded를 기반으로 MSA를 구축한다.
Repository
리포지토리는 영속성 관리를 위해 물리 저장소(DB)에 도메인 객체를 저장하는 구현 모델로,
애그리거트 단위로 조회, 저장을 한다.
이때, 응용 서비스 단이 리포지터리 구현기술의 영향을 받는다는 점이 중요하다. (예) 트랜잭션 처리
# 출처
상사가 추천해주신 책은
'DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기(최범균)'
http://www.yes24.com/Product/Goods/27750871
DDD START! - YES24
DDD의 핵심 개념을 배우고 구현으로 익히기!이 책은 DDD(도메인 주도 설계)를 처음 접하는 개발자를 위한 책이다. DDD를 실제 업무에 적용할 수 있도록 기본적인 이론을 설명하고 이를 구현한 코드
www.yes24.com
에린 에반스의 '도메인 주도 설계'를 기반으로 하고 있다고 한다.
개인적으로 초보가 읽기에 아주 좋은 난이도였다고 생각하지만
다만,
1. 최신 소스 X
- 2016년에 쓰인 책이라 JPA내용 등은 예전 소스가 많았다.
2. 심화 내용이 부족
- 뒷부분으로 갈 수록 설명이 좀 부실한 편이었다.
같은 내용이 많이 반복되기도 했는데, 이해를 돕는 측면이여서 나쁘지는 않았다.
3. 중급자 이상
- 쉽게 쓰여진 책이지만 적어도 Spring과 JPA를 알고 있어야 한다.
이미 Spring에 많이 포함된 기본인 기술영역이라서, 초보가 읽기엔 쉽지 않을 것 같다.
그래도 기초를 이해하는데 도움을 받은 좋은 책이었다.
'Java & Spring' 카테고리의 다른 글
[Java Test] 1. JUnit5 (2) 테스트 필터링, 테스트 반복 (1) | 2023.05.19 |
---|---|
[Java Test] 1. JUnit5 (1) (0) | 2023.05.18 |
[DDD] 도메인 주도 개발 - (3) 리포지토리 & 모델 구현 (0) | 2023.02.21 |
[DDD] 도메인 주도 개발 - (2) 애그리거트 (0) | 2023.02.21 |
[Spring Legacy] 설명 (0) | 2021.01.27 |
[Spring Regacy] 스프링 레가시 프로젝트 만들기 (0) | 2021.01.27 |
PreparedStatement와 Statement의 차이점 (0) | 2021.01.27 |
01. Setup Java JDK and Variables (자바 JDK 설치 및 환경설정) (0) | 2020.06.09 |