DDD 트릴레마 - 도메인 모델 완전성 vs 도메인 모델 순수성
다음 글은 블라디미르 코리코프의 DDD 트릴레마를 번역하고, 부족한 내용을 보충하여 작성한 글입니다.
기초 지식 - 기능 요구사항 vs 비기능 요구사항
- 기능 요구사항
- 비즈니스 로직
- 비기능 요구사항
- 인프라 특성
- 인프라 수준의 구현으로 해결해야 하는 요구사항
- 성능, 품질, etc.
DDD를 왜 쓰는가? → 소프트웨어 핵심에서 복잡성을 해결하는 것
DDD에는 흥미로운 트릴레마가 하나 존재한다.
지금부터 예시를 통해 알아보도록 하자.
예시: 사용자 관리 시스템
사용자 도메인
사용자 도메인의 컨트롤러
이메일 검증 기능 구현 - 이메일 중복 체크
지금부터 새로운 비즈니스 로직을 추가해보겠다. 이메일은 유일해야 하며, 사용자 이메일을 변경하기 전에 시스템은 새 이메일이 이미 사용 중인지 확인해야 한다고 해보자.
도메인 모델의 순수성 챙기기
위 코드의 장점과 단점을 한번 느껴보도록 하자.
장점
- 도메인 로직이 인프라 로직에 의존하지 않음
단점
- 도메인 로직 내에 모든 비즈니스 로직이 존재하지 않음
- 컨트롤러(애플리케이션 레이어)에서 두 이메일의 검증을 수행함
도메인 모델이 더이상 “풍부”해지지 않아졌다는 느낌이 든다.
이게 마음에 들지 않은 우리가, 도메인 모델의 "완전성" 을 챙겨보도록 하자.
도메인 모델의 완전성 챙기기
좀 도메인 모델이 풍부해졌다. 도메인 객체 간 상호작용만으로 모든 로직 처리가 가능해졌다. 하지만 여기엔 치명적인 문제가 있다.
장점
- 도메인 로직이 모든 비즈니스 로직을 구현함.
단점
- 도메인 로직이 리포지토리 인터페이스를 직접 참조함.
- 도메인 로직이 인프라 레이어에 의존함.
- 엥? 도메인은 리포지토리 인터페이스랑 같은 레이어에 존재하지 않나? 이게 왜 문제가 되지?
→ 하단의 참고 1을 봐주세요.
상위 모듈이 하위 모듈에 의존하는 것은, DIP를 완전히 위반하는 형태가 된다.
제 3의 선택지 - 완전성과 순수성 모두 챙기기
장점
- 도메인 로직의 완전성과 순수성을 모두 챙김.
단점
- 모든 User 엔티티를 가져와야 함.
- 불필요한 엔티티까지 전부 가져와야 함.
- 성능이 감당이 안됨.
위와 같은 방식은, 성능을 희생해 도메인 완전성과 도메인 순수성을 챙겼다고 할 수 있다.
DDD 트릴레마: 도메인 완전성, 도메인 순수성, 성능
위와 같은 문제를 일반화하면, 다음과 같은 비즈니스 로직때문에 발생한다고 할 수 있다.
- 저장소에서 데이터 검색
- 비즈니스 로직 실행
- 비즈니스 로직에 따른, 새로운 데이터 검색
- 또 다른 비즈니스 로직 수행
- 데이터를 저장소로 다시 유지
DDD에는 다음과 같은 트릴레마가 존재하며, 아래 세가지 중 두가지만 챙길 수 있다고 알려져 있어, DDD의 CAP이론이라고 불리기도 한다.
- 도메인 모델 완전성
- 모든 애플리케이션의 도메인 로직이 도메인 계층에 위치하는 경우, 즉 단편화되지 않은 경우.
- 도메인 모델 순수성
- 도메인 계층에 프로세스 외부 종속성이 없는 경우.
- 성능
- 불필요한 프로세스 외부 종속성 호출의 존재.
일반적으로는, “도메인 모델 순수성” + 성능을 챙기는 것이 좋다.
- DDD를 사용하는 이유: 소프트웨어 핵심에서 복잡성을 해결하는 것.
- 비즈니스 로직 사이에서, 인프라 로직을 분리시키는 것.
참고 1) 리포지토리는 도메인 레이어에 있는데, 그냥 도메인 엔티티가 리포지토리를 참조해서 사용하면 안되는 이유가 뭐지?
리포지토리가 도메인 레이어에 존재하는 이유를 잘못 파악하고 있다.
- 동일 레이어간 참조가 가능하다고, 도메인 엔티티가 리포지토리를 직접 참조해도 된다는 뜻은 아니다.
- 이런 수준의 인터페이스는, 인프라 로직의 의존을 감추는 “해킹”에 불과하다.
리포지토리가 도메인 레이어에 존재하는 이유
- 애그리거트의 경계 설정 목적
- 도메인 엔티티가 직접 참조하라고 있는게 아님
- 리포지토리가 도메인 레이어에 존재한다는건, 리포지토리, 즉 애그리거트 경계가 “도메인 언어로 간주”된다는 뜻이다.
그럼 도메인 엔티티는 어떤 인터페이스를 참조할 수 있는거지?
- 의미 있는 추상화
- 도메인 모델에 주입되는 인터페이스는 단순히 테스트 목적으로만 존재해서는 안 됨.
- 실제 도메인 개념을 반영하여, 여러 구현체가 존재할 수 있는 진정한 추상화를 제공해야 함.
- 테스트와 설계의 균형
- 단위 테스트를 쉽게 하기 위해 의존성 주입을 남용하는 것은 설계 상의 문제로 이어질 수 있음.
- 즉, 테스트를 위한 '해킹'이 아닌, 도메인 자체의 의미와 책임에 부합하는 구조여야 함.
- 재사용된 추상화의 원칙
- 좋은 추상화란 실제 비즈니스 요구사항을 반영하며, 최소한 두 개 이상의 구현체가 존재해야 한다는 원칙을 지켜야 함.
- 일부에서는 최소 세 개 이상의 구현체를 권장하기도 하는데, 이는 추상화가 단순한 목적으로만 사용되지 않도록 하기 위한 기준.
출처: https://enterprisecraftsmanship.com/posts/domain-model-isolation/
참고 2) 근데, 이메일 유일성 검증이 비즈니스 로직으로 가능한가?
- 불가능하다.
- 유일성 검증은, 비즈니스 로직만으로는 필연적으로 동시성 문제에 부딛치게 된다.
- 그렇다고 DB에 검증로직을 온전히 맡겨버리면, 비즈니스 로직이 인프라 로직에 의존하게 되는 문제가 발생한다!
그럼 어떻게 해야 하나?
- 리포지토리 구현체에서 유니크 제약조건 예외를 잡는다.
- 리포지토리 구현체는 해당 예외를 잡아 비즈니스 로직 커스텀 예외로 바꿔 던진다.
- 비즈니스 로직은 해당 예외를 잡아, 비즈니스 로직으로써 처리한다.
참고 3) 그럼 동시성 문제와 같은 것들은, 모두 비즈니스 로직이 인프라 로직에 의존하는 것 아닌가?
그걸 위해, 우리는 애플리케이션 레이어를 사용하는 것이다.
애플리케이션 레이어는, 도메인 로직이 아닌 성능과 데이터 일관성과 같은 비기능 요구사항을 처리하기 위해, DB에 락을 거는 등 인프라 로직을 위한 도메인 레이어의 인터페이스를 호출하게 된다.