1. DDD
- DDD 아키텍처를 이용하려는 이유 :
- 이전에 공부했던 내용 중에 객체 지향 예시 코드들나 JPA 예시 코드들이 거의 Data-Driven이나 Transactional Script 방식이 아니라 Domain-Driven 방식이라서 공부하게 되었다. 처음에는 이러한 방식이라는 것을 알지 못했으나 공부를 하면서 Domain-Driven라는 것을 알게 되었고 사이드 프로젝트로 ORM을 JPA를 이용하면서 해당 도메인 모델을 도입해보려고 자세하게 알아보게 되었다.
0) DDD 전술적 패턴 vs DDD 전략적 패턴
- 전쟁이랑 똑같은 생각을 하면 된다. 전략 그리고 국지적인 전투에서 사용하는 전술 여기에는 국지적인 바운디드 컨텍스트 내에서 사용하는 전술이 전술적 패턴이다. 이게 바로
DDD의 전술적 패턴
이다.
- 가장 어렵고 복잡한 도메인에 바운디드 컨텍스트 내에서 모델 드리븐하게 개발하는 방법, 모델링 하는 방법 이게 바로
DDD의 전략적 패턴
이다.
- DDD의 전술적 패턴은 큰 그림을 그리는 것이고 DDD의 전략적 패턴은 작은 그림 그리는 것이다. 즉, DDD 전술적 패턴 안에 DDD 전략적 패턴을 포함하고 있다.
1) DDD 전술적 패턴 구조
- 전체적으로 문제 부분을 해결 부분으로 나누는데 해결 부분은
일반
(User, Tenent) 부분,지원
(외부 연결 API 등의 상용 서비스) 부분,핵심
(핵심 비즈니스, 행위) 부분으로 나누어 패턴이 구성된다.
- 핵심 도메인: 비즈니스의 핵심이 되고, 가장 어렵고 복잡한 곳(쇼핑몰의 결제, 쇼핑 서비스)
- 지원 도메인: 핵심 도메인을 도와주는 기능. 부가서비스 (쇼핑몰의 제품 조회, 배송 조회 서비스)
- 일반 도메인: 대부분의 비즈니스에서 필요한 기능 (계정, 이메일 인증 등의 상용서비스로 대체 가능)
2) DDD entity 특징
- DDD 전술적 패턴의 entity에서는 식별성이 생명주기가 있고
행위
가 우선한다. 행위가 그것과 연관된 속성이 중요하다.응집력
이 중요한 패턴이다.
3) DDD Lite에서 model driven으로 설계 하는 경우
핵심
부분에서Model Driven
으로 DDD를 설계한다.
- 중요** :
Model Driven
에서 속성이 먼저가 아니라행위
가 먼저다. 행위가static 메서드
에 포함되어 생성된다!!
4) 값객체(Value Object)
- 식별성은 존재하지 않지만 도메인의 특정 영역을 추상화시키는 것을 ‘값객체’라고 한다.
- 예시에서는 ‘Content~~’를 포함한 객체들을 묶어서 ‘content’라는 값객체로 단순화시키고 불변성을 가지고 값객체에 행위를 추상화시킬 수 있다.
- 보통은 수량, 돈, 주소 등을 보통 ‘값객체’로 정한다.
- 값객체는 접미어가 같은 형태로 묶어서 사용한다.
- 값객체 안에 값객체가 또 있어도 되고 entity가 있어도 된다.
5) Aggregate
불변식
을 의미한다. 간단히 말하면, 도메인 모델에 일반식을 지키는 규칙이다. 데이터 변경의 단위가 된다!!- ex) DB에서는 -> Transaction 단위가 된다. // 분산환경에서는 -> ‘Lock의 범위’가 된다.
- Aggregate는 집합을 의미한다.
- 초기 도메인 모델 설계시, PageUser과 PageFile을 묶어서 모델을 설계했지만 요구사항, 유스케이스에 따라 도메인 모델 정제를 통해 이 2가지를 분리했다.
- ex) ‘PageFile 하나만 다운로드하게 해줘’, ‘업로드하게 해줘’ 등
- ‘AggregateRoot’라는 스테레오타입이 있는데 이것은 각 Aggregate을 통과하는
Gateway
역할을 해준다.
a. AggregateRoot 첫 번째 규칙** :
- 예를 들면, PageHistory라는 Repository는 PageUser에 접근하기 위해서
항상 AggregateRoot인 Page Reposiotory를 무조건 거쳐서
PageUser Repository에 접속할 수 있다.**
- 중요**: Aggregate이 DDD-Lite에서 가장 중요하다. 결론적으로 나온 도메인 모델 설계 결과는 전체 Entity는 4개 인데 AggregateRoot는 3개이다. 그래서, AggregateRoot을 잘 설계하는 것이 가장 중요하고 추상화에 큰 도움이 되기 때문에 가장 중요하다. 지속적으로 도메인 모델 지식 탐구를 통해서 어떻게 경계를 잡고 항상 도메인 모델 정제를 해야한다.
b. AggregateRoot 두 번째 규칙** :
Aggregate 하나 당 Repository는 하나만 만들어야 한다.
여기서는 Entity는 4개 인데 AggregateRoot는 3개라는 결론이 도출되었고 Repository도 총 3개이다. PageUser는 Repository가 없고 항상 Page repository를 통해서만 접근할 수 있다.
- JPA 코드를 보면,
@Version
이라는 것이 있는데 여기서는 JPA가 제공해주는 ‘낙관적 오프라인 락’을 구현하기 위해 사용했고, 영속성 전이를 구현하기 위해CascadeType.ALL
을 이용했다.(PageEntity인 PageUser 속성의 뭔가 값에 변화가 일어나면 영속성전이가 일어나면 알아서 save나 update가 된다.)
@Entity
public class Page {
@Id
private Long pageId;
@Version
private long version;
// ...
@OneToMany(mappedBy = "page", cascade = CascadeType.ALL)
private List<PageUser> pageUsers = new ArrayList<>();
}
6) 도메인 이벤트 패턴
- 발행, 구독 모델을 기반으로 해서 구현을 지원한다. 그래서, 결합도를 많이 낮춘다.
- 이벤트 방식의 후속 구독은 트랜잭션에 포함되지 않는다.
- 이벤트를 발행 입장과 구독 입장을 구분해셔 분리!!
7) Application Service
- 다음의 그림은 시퀀스 다이어그램의 간소화 그림이다.
- 트랜잭션이나 보안을 처리하는
Thin
구조,Facade
(파사드) 패턴 방식을 포함하고 있다.
a. Application Service 핵심 내용** :
Application Service
에는 트랜잭션이나 보안을 처리하는 로직은 가지고 있을 수 있지만,Domain 로직을 절대 포함하지 말 것!!
- 도메인로직을 로딩해서 단지 도메인 로직을 위임한다. 그래서
Thin
구조라고 부른다. 그래서 Facade하고 Thin하다.
- Application Service는
유스케이스의 표현
이라고 부른다. 즉, 유스케이스 하나가 애플리케이션 서비스 1개라고 정의된다.
- Application Service에서 각각 구독한다. 도메인을 구독하는 책임은 Application Service 책임이다. Application Service에는 구독과 발행이 있는데 발행할 때는 적어도 관심사별로 나누어서 설계해야 한다.
b. Application Service 구현하는 방식 :
- Application Service를 구현하는 방식은 스프링 데이터(
Trigger가 필요
하다, Repository에 항상save
메서드를 호출해야 한다.)에 의존, 직접 구현 하는 방식(AOP
,Thread Local
이용하여 구현)이 있다.
c. Application Service 예시 :
Page
객체를PageRepository
에서 조회 후changeContent
메서드로 위임하게 된다.내부
에서도메인 로직
을 처리하고 마지막으로도메인 이벤트
를등록
하고 어딘가로 보내준다. 어딘가로 말하는 부분은이벤트를 발행
하고구독
하는 부분은 프레임워크나 애플리케이션쪽 관심사라서 내부 로직에 숨겨져있고추상화
처리되어 있다.이벤트 프로세서
가구독
할 수 있게 각각바인딩
해주고구독
할 수 있게 해주고 이즉시
트랜잭션
은종료
된다. 트랜잭션 종료 타이밍은 옵션으로 늘릴 수도 있다.
- 애플리케이션 서비스에서 각각 필요에 맞게 구독하게 된다. 페이지 이력도 구독도 하고 알림 센터에 푸쉬도 하고 발행하고 검색 인덱싱도 바꾸고 이러한 일련의 과정들이다.
- 애플리케이션 서비스에서 각각 구독한다. 도메인을 구독하는 책임은 애플리케이션 서비스의 책임이다. 발행할 때는 적어도 관심사별로 나누어서 설계해야 한다.
8) 도메인 이벤트를 구현하는 방식
- 에릭에반스 시절에 없던 새로 발견된 패턴이다.
a. 스프링 데이터
에 의존하는 방식
- 스프링 데이터에 의존하여 도메인 이벤트를 구현하면, 항상
트리거
가 필요하다, 하지만, 원래 DDD에서는 트리거 없이 자동적으로 이벤트가 발행하도록 권고되어 있지만, 스프링은 뭔가 기술적으로 한계가 있다. 그래서 Repository에save
메서드를 호출해야 이벤트가트리거
된다. 이렇게 하여 이벤트를 발행하고 객체를 변경할 수 있다.
- 원래는 save 메서드 없이 자동적으로 객체가 변경이 되어야 한다. 자체로 더티체킹해서 이벤트를 발행하여 객체를 변경하는 꼴이다. 그래서 직접 구현을 하는 경우도 있다.
- Repository#save(T) : Trigger 역할!
스프링 데이터
에서 사용하는 도구 :@DomainEvents
,@AfterDomainEventPublication
,AbstractAggregateRoot
b. 직접 구현
하는 방식
-
Thread Local
이나AOP
이용
9) 실습 코드 1 ( Data-Driven vs Domain-Driven )
a. Data-Driven 방식 :
의존성
이 높다. 빈약한 도메인을 넘어서 단순히DTO 수준
이다.
- Transactional Script 방식
A. 예시 코드 : 너무 set, set, set 해버린다.
// PageCommandService
@Transactional
public void updateContent(...){
wikiService.checkUpdate(wikiId); // 1. 해당 위키에 대한 권한 체크
Page page = pageDao.selectOne(pageId);
this.checkUpdate(pageId, member); // 2. 해당 페이지에 대한 권한 체크
page.setContent(content); // 3. 페이지 내용 등 변경**
page.setUpdatedAd(LocalDateTime.now());
page.setUpdateMemberId(member.getMemberId());
pageHistoryService.create(page); // 4. 페이지 변경 내역 추가
streamService.push(page); // 5. 두레이 스트림 발행
searchService.replace(page); // 6. 검색 변경 알림
}
b. Domain-Driven 방식 :
- Page에 해당하는 최소한의 의존성을 가지고 있다. 풍부한 도메인 모델을 가지고 있다.
A. Application Service 예시** : PageHistoryService**
- 이전 예시에서 PageHistoryService는 이벤트 발행하고 구독해주는 서비스 중에 하나다. 즉, 페이지 이력을 등록해주는 역할을 해준다.
- 트랜잭션에 미포함되기 때문에
@Transactional
:AFTER_COMMIT
(이전에 내용 변경된 트랜잭션이 커밋이 되고 나서 실행이 되고) +REQUIRES_NEW
(이전에 트랜잭션이 커밋이 되고 나서 실행이 되고나서는 새로운 트랜잭션이 없기 때문에 새로운 트랜잭션을 생성해준다.)
- Application Service라서 이 곳에 도메인 로직은 없어야 하고 도메인 로직에 관한 모든 것은 도메인 서비스에 위임했다. 그래서 Application Service은 Thin, Facade 특징이 있다.
B. Domain-Driven 방식 예시 코드** : 풍부한 도메인 모델을 이용하여 깔끔하다.
- Application Service은 로직에 필요한 인자들을 파라미터로 받고 pageRepository에서 단순히 조회해서 해당 도메인 모델이 갖고 있는 도메인 서비스(changeContent)를 불러내면 끝이다!!**
- 아래 예시 코드와 위의 예시 코드를 비교하면,
Application Service
라는 곳은 모든 도메인 로직을없애고
모든 도메인 로직은 도메인 모델이 갖고 있는도메인 서비스
에위임
하여 사용한다. 그래서Thin
하고Facade
하다.
// PageCommandService
@Transactional
public void updateContent(Tenant tenant, Long wikiId, Long pageId,
PageContentUpdate command, WikiAccessible updator){
Page page = pageRepository.findById()
.orElseThrow(() -> new PageNotFoundException(pageId));
page.changeContent(wikiId, command.getBody, updator);
// return pageRepository.save(page);
}
10) 실습 코드 2 ( Data-Driven vs Domain-Driven )
a. Data-Driven
A. PageCommandService
- 여러 애플리케이션 서비스들이 한 곳에 묶여 있어서 결합도가 매우 높다. 응집도가 높다.
@Service
public class PageCommandService {
PageRepository pageRepository;
WikiService wikiService;
WikiMemberService memberService;
PageHistoryService pageHistoryService;
DoorayStreamService streamService;
SearchService searchService;
}
B. Page
-
Page 객체는 도메인 서비스가 없는 빈약한 도메인 모델이다.
-
그래서, 단순히 setter, getter만 있는 DTO 수준이다.
// Page 객체
public void setContent(Content content){
this.content = content;
}
public void setUpdateMemberId(Long memberId){
this.memberId = memberId;
}
public void setUpdatedAt(LocalDateTime dt){
this.updateAt = dt;
}
b. Domain-Driven :
A. PageCommandService
- 여러 애플리케이션 서비스가 필요없고 하나의 애플리케이션 서비스에서 도메인 모델이 갖고 있는 도메인 서비스를 이용하기 때문에 Domain-Driven 방식을 이용하면, 매우 간단히 구성된다.
@ApplicationService
public class PageCommandService {
pageRepository pageRepository;
}
B. Page
- 도메인 서비스가 도메인 모델에 포함되어 풍부한 도메인 모델의 모습이다.
// Page 객체
public void changeContent(Long wikiId, Content newContent, WikiAccesor who){
checkUpdatable(who);
this.content = newContent;
setLastUpdateWikiMemberId(who.getWikimemberId());
// 이벤트 등록
registerEvent(new pageContentChanged(
this.pageId, who));
}
C. PageHistoryService
- 도메인 모델의 도메인 서비스를 단순히 위임받아 사용하는 Thin하고 Facade한 애플리케이션 서비스의 모습이다.
// @Async : 비동기도 가능하다.
@TransactionalEventListener(phase: TransactionalPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void appendHistoryBy(PageContentChanged event){
PageEvent pageEvent = pageEventStore.append(event);
pageHistoryAppender.appendFrom(event.getPageId());
// 페이지 변경 내역 추가
}
11) ‘도메인 서비스’ 개념**
- 무상태에서 도메인 서비스를 처리하는 로직으로서 엔티티에 Aggregate과 같은 속성과 행위를 다 가지고 있는 것이 아니라
행위
만 가지고 있는 것이다. 속성이 없다. 정리하면속성이 없다
. 이러한 것을무상태
라고 한다.
- Application Service와 구분하여 비교하기!!
12) DDD 팁 : JavaDoc**
-
이벤트 발행, 구독 모델의 단점은 추적 불가해서 문제가 있다. 누가 발행을 했는지 누가 구독을 했는지
JavaDoc
을 달아서 헤깔리지 않게 확인한다!! -
아래 실습 코드의 실제 PageContentChanged 도메인 모델은 pageId, occrrer, occuredOn을 파라미터로 갖고 있으며,
addPageHistory()
(변경 이력 추가),publishDoorayStream()
(스트림 발행),replaceSearchDocument()
(해당 문서의 내용 변경)를 도메인 서비스로 갖고 있다.
/**
* 페이지 내용 변경됨 이벤트
*
* @see Page#changeContent(Long, Content, WikiAccessor)
* @see PageHistoryAppender#appendFrom(PageContentChanged)
* @see DoorayStreamAdapter#pushFor(pageContentChanged)
* @see RabbitmqSearchIndexer#replaceDocumentFor(PageContentChanged)
*/
@Value
public class PageContentChanged extends AbstractPageEvent {
private Long pageId;
private Content content;
private WikiAccessor accessor;
public PageContentChanged(Page page, Content content, WikiAccessor accessor){
// 생략
}
}
13) 개념 모델 구독 확인 코드
- 이벤트는 항상
실행순서
를 보장해주지 않는다! 구독하는 입장에서는@see
(code on)를 항상 추가해서멱등성
(다음 구독 할것 들어왔는데 이전 것을 또 처리하면 안되니까 한 것을 또 구독하지 않게 해준다)있게 설계 가능!!
@see
태그는 관련 항목으로 외부 링크 또는 텍스트를 표시하거나, 다른 필드나 메소드에 대한 모든 참조 링크를 나타내는 경우에 사용한다.
14) JpaRepository 문제점
- Spring에 너무 의존적이라서! 문제가 있다고 판단된다. 원래 Repository는 POJO로 설계해야 하는데 trade-off가 있어서 고민은 해봐야 한다.
- Spring의 Repository 애노테이션은 DDD의 Repository를 지원하기 위해서 나온 애노테이션을 의미한다. JavaDoc에 DDD와 관련된 이야기가 나오며, DDD만으로 사용하지 않아도 되고 Data Persistence Layer에 액세스 용도로 써도 된다고 완화시켜서 설명해놓았다.
2. 헥사고날 아키텍쳐
1) 헥사고날 아키텍쳐 개념
a. 해당 아키텍쳐가 필요한 이유 :
- 우리는 DDD를 설계 시, 어디에다가 구현하고 배치해야 하는지? 잘 모르겠다. 그래서, 헥사고날 아키텍쳐가 필요했음을 공부를 통해 알게 되었다. Layered Architecture는 치명적인 단점이 존재한다.
- 최근에는 헥사고날 아키텍쳐를 사용하지 않을 수 있다.
b. 구조 :
- 중심에는
Domain
영역, 그다음 바로 밖에는Application
영역, 가장 바깥에는Adapter
영역이 있다.
c. 흐름도 설명 :
Primary Adapter
는Driving
(인입 지점)이고Secondary Adapter
는 기술적으로 구현을바인딩
하는 부분이다. 일반적으로Adapter
는 데이터를바인딩
하는 부분을 의미한다. Adapter는기술
에 대한포트
,Adapter 패턴
이라고 부른다.
Primary Adapter
영역 :- 요청을 받아서 응답을 하는 부분이다. REST API, CLI Batch 등이 있다.
Secondary Adapter
영역 :- 기술적 처리에 대한 통로이다. 영속화를 담당하는 DB, 캐싱을 담당하는 Redis, 메시징을 담당하는 RabbitMQ과 같이 인프라 또는 다륻 서비스를 호출하는 것 등이 있다. 기술적인 개념들을 바인딩하는 부분이다.
Domain
영역 :- 도메인 모델들이 도메인 영역이다.
Application
영역 :- 애플이케이션 서비스를 의미한다.
2) 헥사고날 아키텍쳐를 앞선 Dooray Wiki 예시에 적용
- Dooray Wiki에서 본문을 수정하는데 필요한 컴포넌트를 나열한 모습이다.
- PageController :
Primary Adapter
영역(요청, 응답의 영역이라서)
- Page :
Domain
영역(도메인 모델 그 자체라서)
- PageContentChanged :
Domain
영역(행위 메서드라서)
- PageRepository :
Domain
영역(특이하게 이것도 도메인 영역에 포함된다! AggregateRoot라서)
- PageCommandService :
Application
영역
- PageRepositoryImpl : 영속화된 기술적 구현이 있기 때문에
Secondary Adapter
이다.
a. 헥사고날 아키텍쳐의 핵심** :
- 모든 화살표가 바깥쪽에서 안쪽으로 모아든다. 여기서 화살표는
의존성
의 화살표이다. 데이터 흐름의 화살표가 아니다!!
- 이러한 개념과 비교하여 계층형 아키텍쳐의 치명적인
단점
은도메인
이인프라
에의존
한다. 도메인적 관심사와 기술적 관심사가 섞이고 있다.복잡성
이 폭발적 증가!!- 그래서, 도메인적 관심사와 기술적 관심사 그리고 애플리케이션 관심사를 완전히
분리
해야 한다.
- 그래서, 도메인적 관심사와 기술적 관심사 그리고 애플리케이션 관심사를 완전히
- 이벤트 구독 후도 화살표가 바깥쪽에서 안쪽으로 흐르는데 Dooray Wiki 예시를 보면, Adapter 부분에서 NotificationService를 구현하는 것이 DoorayStream인데 이 부분은 SOLID 원칙 중에서 DIP 규칙으로 설계된 모습을 볼 수 있다. 이것을 통해서 의존성 방향을 역전시킨 것이다. 이렇게 설계하면
도메인 모델
의순수성
을 유지시킬 수 있도록 망을 구축한다.
3) 헥사고날 아키텍쳐 패키지 구조
a. 모듈 예시 :
- Adapter 영역만 알아보면 다음과 같다.
A. presentation 영역
- presentation 영역은 표현 영역으로서, 각종
controller
역할로 요청과 응답을 처리하는 영역이다. 예를 들면, CLI, Web, REST API 등이 있다.
B. infrastructure 영역
- 이 곳에는 실제 기술들이 포함되는 영역이다. 예를 들면, ElasticSearch, JPA, RabbitMQ, Redis 등이 있다.
C. service 영역
- 이전에 본 예시처럼 AccountService와 ProjectService에 WikiService가 의존했었던 영역이다.
D. configuration 영역
- Spring이 자바 Config 파일을 넣는 패키지들이다. 원래는 프로젝트 Root 폴더인 최상단 경로에 포함되지만 프레임워크도 기술이기 때문에 Adapter 영역에 포함되는 것도 추천한다. 상관은 없지만 추천한다.
E. 특이한 점
- RabbitMQ : RabbitMQ 같은 경우는 메시지 브로커라서 나가는 통로도 되지만 들어오는 통로도 가능하다. 들어오는 통로는
Primary Adapter
이면서도 이벤트로 누군가가 들어오면 내가 받아줘서 구독해줘야하므로 즉 핸들링 해줘야하니까Secondary Adapter
역할도 할 수 있다.
4) 정리 및 요약
- 기술보다는 도메인이 먼저다. 도메인을 바라보고 기술을 선택하자!
- 마틴 파울러가 작성한
Pattern Of Enterprise Application Architecture
저서에서 나온 Data-Driven하게 개발할 때, 복잡성과 Domain-Driven하게 개발할 때, 복잡성을 이용하여복잡성 증가 차이 그래프
를 이전에 예시를 든 Dooray Wiki 서비스에 적용시켜보자!일반
,지원
서비스는 도메인 난이도가 복잡하지 않아서 그냥 기존대로Transactional Script 패턴
이나Data-Driven
방식으로 설계해도 된다. 하지만, Application의 핵심이 되는핵심 도메인
은Domain-Driven
방식으로DDD 전술적 패턴
을 적용하게 되면, 복잡성을 제어할 수 있을 것이다!!