본문 바로가기
Web Development/Design, Architecture

[DDD] 도메인 주도 설계가 잘 안 됐던 이유

by saltyzun 2024. 7. 18.

프로젝트를 시작할 때 'DDD를 사용해보자' 라는 말이 종종 나오곤한다. 이후로 이벤트스토밍을 하며 요구사항을 분석하고, 포트앤 어댑터 계열의 아키텍처에 Aggregate(Root), Entity VO, Repository 등 DDD 요소들을 활용하여 어플리케이션을 설계한다. 그러나 막상 이러한 설계대로 어플리케이션을 구현하다보면 많은 난관에 부딪힌다(물론 잘 끝날 수도 있다). 왜 그럴까? 내가 놓치고 있던 부분은 무엇일까? 이 글에서는 내가 그동안 DDD 를 한답시고 놓친 것들에 대해 얘기해보고자 한다.


 

 

1. 조회는 DDD 의 관심사가 아니다


실제 웹어플리케이션 로직을 보면 커맨드 뿐 아니라 쿼리 역시 꽤나 복잡하다. 심지어 ERD 엔티티 간의 참조무결성이 완벽히 지켜지지 않아 쿼리로 해결이 안 돼 어플리케이션에서 직접 조인을 구현해야 하는 경우도 존재한다. 그러나 DDD는 조회에 큰 관심을 두고 있지 않다. 그러다보니 DDD 설계를 하면서 조회를 크게 고려하지 못한다. 특히나 개발하고 있는 시스템이 뒷단의 외부기관의 API나 내부의 단위 시스템 API를 Aggregation 해주는 역할이라면, 커맨드 만큼이나 복잡한 쿼리가 많을 수 있다.

 

그나마 DDD에서 조회를 유의미하게 다루는 부분으로는 CQRS 파트가 있다. 그러나 이를 실제로 구현하기 위해서는 레퍼지토리에 존재하는 주요한 파인더 메소드를 대체할 수 있을만큼 상당히 정교한 읽기모델이 필요하며, 이 읽기모델은 여러 도메인 이벤트를 수신하여 처리해야 하기에 나름의 복잡성이 존재한다. 또한 실제 운영중인(또는 예정인) 프로덕트라면 비즈니스 로직이 변경되었을 때 읽기 모델의 과거 데이터를 어떻게 처리해줄지, DB의 일부 데이터 수정 또는 롤백 등의 이슈가 발생했을 때 이를 어떻게 처리해줄지 등도 함께 고려해 보아야 한다. 따라서 CQRS 를 설계 및 구현하고자 할 때는 이런 트레이드 오프를 반드시 고려해야 한다.

 

아래는 반 버논이 쓴 ⌜도메인 주도 설계 구현⌟ 에서 CQRS에 대해 다루고 있는 주요 내용과 블라드 코노노프가 쓴 ⌜도메인 주도 설계 첫걸음⌟에서 시스템 복잡도에 대해 다루고 있는 부분이다. 

사용자가 필요로 하는 데이터 뷰를 리파지토리로 쿼리하기란 어려울 수 있다. 특히 UX 설계가 여러 애그리거트 타입과 인스턴스로부터 데이터 뷰를 생성해야 할 때가 어렵다. 여러분의 도메인이 정교할수록 이런 상황이 발생할 가능성이 높아진다.

일반적으로 커맨드 메소드와 쿼리 메소드를 모두 갖고 있는 애그리거트를 확인하게 된다. 또한 특정 속성을 필터링하는 여러 파인더 메소드를 갖고 있는 리파지토리를 만난다. 하지만 CQRS와 함께라면 이런 ‘일반성’을 무시하고, 보여줄 데이터를 쿼리하는 방법을 다르게 설계할 것이다.

- 어플리케이션 서비스 인터페이스를 커맨드 / 쿼리로 분리한다
- 커맨드 모델에서 발행하는 모든 도메인 이벤트를 모든 쿼리 모델이 수신하여 처리한다.

⌜도메인 주도 설계 구현⌟
복잡성이라는 주에는 프로그램의 한 부분에 대한 로컬 복잡성을 최소화 하려는 단순한 시도보다 더 많은 것이 있다. 더 중요한 복잡성의 유형은 글로벌 복잡성, 즉 프로그램 또는 시스템의 전체적인 구조에 관한 복잡성이다. 예를 들어, 시스템의 주요 요소들이 연관되거나 상호 의존하는 정도다.

⌜도메인 주도 설계 첫걸음⌟

 

2. 전술적 설계보다 전략적 설계를 먼저 생각해야 한다


이전까지 나는 DDD 가 잘 안 된다고 느낄 때마다 'DDD 에 대한 지식이 부족하기 때문이 아닐까' 생각했다. 여기서 생각하는 DDD에 대한 지식은 대부분 '애그리거트가 이렇게 구성되지 않았다면 트랜잭션으로 훨씬 간단하게 해결할 수 있었을 텐데' 와 같은 전술적 설계의 영역이었다. 그러나 ⌜도메인 주도 설계 첫걸음⌟ 에서 아래 구절을 읽은 뒤로 내가 놓치고 있던 건 전략적 설계임을 깨닫게 됐다.

안타깝게도 많은 사람들이 도메인 주도 설계가 팀의 모든 사람이 DDD 전문가인 이상적인 조건에서 그린필드 프로젝트에만 적용할 수 있다고 잘못 생각한다. 역설적이게도 DDD의 혜택을 가장 많이 받을 수 있는 프로젝트는 브라운필드 프로젝트다. 이는 이미 비즈니스 가능성을 입증했고 축적된 기술 부채와 설계 엔트로피에 맞서 대대적인 변화가 필요한 프로젝트를 말한다. 공교롭게도 소프트웨어 엔지니어는 경력의 대부분을 이러한 브라운필드, 레거시, 커다란 진흙 덩어리 코드베이스에서 보낸다.

⌜도메인 주도 설계 첫걸음⌟

 

물론 여기서 말하고자 하는 건 유비쿼터스 언어, 바운디드 컨텍스트와 같은 단순한 전략적 설계의 요소가 아니라, '우리 프로젝트는 어떤 성격을 갖고 있는지', '우리는 어떤 어플리케이션을 만들고 있는지' 와 같은 전략적 사고의 중요성이다.

 

결국 DDD 핵심은 ‘Problem space 를 Solution space 로 어떻게 잘 매핑하는냐’ 라고 생각한다. 즉, 개발자는 스스로가 도메인 전문가의 멘탈 모델을 도메인 모델에 얼마나 잘 녹여내고 있는지를 항상 생각해봐야 한다. 그런데 그린필드 프로젝트의 경우 책에서 얘기한 브라운필드 프로젝트의 성격과 반대로 (1) 비즈니스 가능성이 입증되지 않았고, (2) 기술 부채나 무질서한 설계 등이 존재하지 않는다. 다시 말해 도메인 전문가가 생각하고 있는 문제 영역 자체도 그만큼 모호하고, 문제와 해결 방안이 시장의 반응에 따라 계속해서 변화할 수 있는 것이다.  이러한 프로젝트를 수행할 때는 개발자가 제 아무리 도메인 요구사항을 잘 이해하고 어플리케이션에 전술적 설계요소를 잘 반영해도 시간이 지남에 따라 과거의 설계가 안 좋은 설계로 변화할 수 있다. 따라서 도메인이 불안정하다고 느낄 때면 당장 전술적 설계에 많은 공을 들이기 보다 가능한 빠르게 프로덕트를 개발하고, 피드백을 반영해 리팩토링 하는 과정 속에서 전술적 설계의 일부 요소들을 코드에 녹여내는 게 더 좋다는 게 나의 생각이다.

 

번외로, 애그리거트와 불변식을 유연하게 다루지 못했던 것 역시 전술적 설계를 우선시했기에 범해왔던 실수라고 생각한다. 에릭 에반스에 따르면 애그리거트란 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음이다. 그리고 불변식은 데이터가 변경될 때마다 유지돼야 하는 일관성 규칙을 의미한다. 그래서 우리는 대부분 불변식 단위로 애그리거트를 설계하게 된다. 불변식이 애그리거트 내에 존재하면 트랜잭션 일관성으로 이를 보장해줄 수 있기 때문이다. 보통의 DDD 서적은 이러한 애그리거트를 가능한 작게 설계하는 게 좋다고 한다. 가령 최범균 님이 쓰신 ⌜도메인 주도 설계 시작하기⌟ 에는 경험상 애그리거트에 엔티티가 대부분 하나였다는 얘기가 나온다.

 

그런데 실제로는 어플리케이션의 특성상 불변식의 범위가 굉장히 넓은 케이스도 분명 존재한다. 첫 번째 주제에서 다뤘던 Aggregation 역할을 하는 중간계층의 서버군들이 그 대표적인 예다. 이럴 때 내가 했던 실수는 책에서 배운대로 애그리거트는 가능한 작게 설계하고, 나머지 불변식은 이벤트 등을 통해 결과적 일관성으로 보장하려고 했던 점이다. 서비스 계층의 코드가 방대해지는 걸 막으면서도 가능한 작은 애그리거트를 유지하려고 택했던 방법이다. 그러나 이러한 설계는 되려 시스템 전체의 복잡도를 높인다. 동일한 불변식을 트랜잭션으로 보장하는 것에 비해 결과적 일관성으로 보장하기 위해선 훨씬 많은 요소들(이벤트 재처리 로직이나 배치 프로그램 등)이 필요하기 때문이다. 그렇다고 무조건 넒은 애그리거트가 좋다는 건 아니다. 필요 이상으로 넓은 애그리거트는 응답 속도 저하나 불필요한 요청 실패를 유발하기 쉽고 이후에 시스템을 분리할 때 골칫덩어리가 되기 쉽다. 따라서 내가 개발하고 있는 프로젝트의 성격을 잘 파악하고, 트레이드 오프를 잘 고려하여 적절한 크기의 애그리거트와 서비스 계층을 설계할 필요가 있다. 개인적으로는 애그리거트는 작게 설계하려고 노력하되, 불변식은 서비스 계층에서 @Transactional 을 적절하게 이용해주는 게 적당한 타협점이 아니었나 싶다.

 

3. 아키텍처는 DDD 의 관심사가 아니다.


바로 위에서 애그리거트의 범위가 너무 커지면 향후에 시스템을 분리하기 어려워질 수 있다고 얘기했다. 분산 트랜잭션을 처리하는 게 시스템 분리에 큰 영향을 미친다는 건 많은 개발자들이 인지하고 있는 부분이며, 따라서 초기 설계부터 애그리거트를 하나의 마이크로 서비스가 될 수 있는 대상으로 이해하고 어플리케이션을 설계하게 되는 일도 생긴다. 그러나 이러한 설계는 의존성 관리를 하기 힘들고, 더 나아가 코드를 보며 도메인을 이해하기 어렵게 만든다. 구현하고 있는 onEvent() 함수가 많아질수록 내가 개발하고 있는 모듈(대표적으로는 클래스)의 응집도가 떨어지고 있는 건 아닌지 다시 고민해봐야 한다.

 

실제로 DDD 에서는 MSA 와 같은 아키텍처를 주된 관심사로 다루지 않는다. 전체적인 시스템 복잡성은 고려하지 않은 채 코드 수준 아키텍처만 고려하여 얕은 모듈을 만들지 않도록 주의해야 한다. 시스템에 부하가 심해지거나, 큰 조직개편이 발생하지 않는 이상 서비스는 생각보다 쪼개질 일이 많지 않다. 또한 설령 시스템을 분리한다 해도 내가 만든 서비스 수준이 아닌 회사 전체의 시스템 규모와 비즈니스 응집도를 고려하여 분리될 것이다.

 

참고문헌 

  • ⌜도메인 주도 설계 구현⌟ (반 버논)
  • ⌜도메인 주도 설계 첫걸음⌟ (블라드코노노프)
  • ⌜도메인 주도 설계⌟ (에릭 에반스)
  • ⌜도메인 주도 설계 시작하기⌟ (최범균)
반응형

댓글