Article/도메인 주도 설계 이해하기

DDD 트릴레마 - 도메인 모델 완전성 vs 도메인 모델 순수성

조금씩 차근차근 2025. 4. 1. 22:50

다음 글은 블라디미르 코리코프의 DDD 트릴레마를 번역하고, 부족한 내용을 보충하여 작성한 글입니다.

기초 지식 - 기능 요구사항 vs 비기능 요구사항

  • 기능 요구사항
    • 비즈니스 로직
  • 비기능 요구사항
    • 인프라 특성
    • 인프라 수준의 구현으로 해결해야 하는 요구사항
    • 성능, 품질, etc.

DDD를 왜 쓰는가? → 소프트웨어 핵심에서 복잡성을 해결하는 것

 

DDD에는 흥미로운 트릴레마가 하나 존재한다.

지금부터 예시를 통해 알아보도록 하자.

예시: 사용자 관리 시스템

사용자 도메인

도메인 모델 엔티티. 비즈니스 로직에 집중하고 있는 "풍부한 도메인 모델"이라고 할 수 있다.

사용자 도메인의 컨트롤러

애플리케이션 레이어의 컨트롤러.

이메일 검증 기능 구현 - 이메일 중복 체크

지금부터 새로운 비즈니스 로직을 추가해보겠다. 이메일은 유일해야 하며, 사용자 이메일을 변경하기 전에 시스템은 새 이메일이 이미 사용 중인지 확인해야 한다고 해보자.

도메인 모델의 순수성 챙기기

위 코드의 장점과 단점을 한번 느껴보도록 하자.

장점

  • 도메인 로직이 인프라 로직에 의존하지 않음

단점

  • 도메인 로직 내에 모든 비즈니스 로직이 존재하지 않음
  • 컨트롤러(애플리케이션 레이어)에서 두 이메일의 검증을 수행함

도메인 모델이 더이상 “풍부”해지지 않아졌다는 느낌이 든다.

이게 마음에 들지 않은 우리가, 도메인 모델의 "완전성" 을 챙겨보도록 하자.

도메인 모델의 완전성 챙기기

좀 도메인 모델이 풍부해졌다. 도메인 객체 간 상호작용만으로 모든 로직 처리가 가능해졌다. 하지만 여기엔 치명적인 문제가 있다.

장점

  • 도메인 로직이 모든 비즈니스 로직을 구현함.

단점

  • 도메인 로직이 리포지토리 인터페이스를 직접 참조함.
    • 도메인 로직이 인프라 레이어에 의존함.
    • 엥? 도메인은 리포지토리 인터페이스랑 같은 레이어에 존재하지 않나? 이게 왜 문제가 되지?
      → 하단의 참고 1을 봐주세요.

상위 모듈이 하위 모듈에 의존하는 것은, DIP를 완전히 위반하는 형태가 된다.

제 3의 선택지 - 완전성과 순수성 모두 챙기기

_userRepository.GetAll() 메소드를 통해 모든 사용자를 가져오는 방식.

장점

  • 도메인 로직의 완전성과 순수성을 모두 챙김.

단점

  • 모든 User 엔티티를 가져와야 함.
    • 불필요한 엔티티까지 전부 가져와야 함.
    • 성능이 감당이 안됨.

위와 같은 방식은, 성능을 희생해 도메인 완전성과 도메인 순수성을 챙겼다고 할 수 있다.

DDD 트릴레마: 도메인 완전성, 도메인 순수성, 성능

위와 같은 문제를 일반화하면, 다음과 같은 비즈니스 로직때문에 발생한다고 할 수 있다.

  • 저장소에서 데이터 검색
  • 비즈니스 로직 실행
  • 비즈니스 로직에 따른, 새로운 데이터 검색
  • 또 다른 비즈니스 로직 수행
  • 데이터를 저장소로 다시 유지

DDD에는 다음과 같은 트릴레마가 존재하며, 아래 세가지 중 두가지만 챙길 수 있다고 알려져 있어, DDD의 CAP이론이라고 불리기도 한다.

  • 도메인 모델 완전성
    • 모든 애플리케이션의 도메인 로직이 도메인 계층에 위치하는 경우, 즉 단편화되지 않은 경우.
  • 도메인 모델 순수성
    • 도메인 계층에 프로세스 외부 종속성이 없는 경우.
  • 성능
    • 불필요한 프로세스 외부 종속성 호출의 존재.

일반적으로는, “도메인 모델 순수성” + 성능을 챙기는 것이 좋다.

  • DDD를 사용하는 이유: 소프트웨어 핵심에서 복잡성을 해결하는 것.
  • 비즈니스 로직 사이에서, 인프라 로직을 분리시키는 것.

참고 1) 리포지토리는 도메인 레이어에 있는데, 그냥 도메인 엔티티가 리포지토리를 참조해서 사용하면 안되는 이유가 뭐지?

리포지토리가 도메인 레이어에 존재하는 이유를 잘못 파악하고 있다.

  • 동일 레이어간 참조가 가능하다고, 도메인 엔티티가 리포지토리를 직접 참조해도 된다는 뜻은 아니다.
  • 이런 수준의 인터페이스는, 인프라 로직의 의존을 감추는 “해킹”에 불과하다.

리포지토리가 도메인 레이어에 존재하는 이유

  • 애그리거트의 경계 설정 목적
  • 도메인 엔티티가 직접 참조하라고 있는게 아님
  • 리포지토리가 도메인 레이어에 존재한다는건, 리포지토리, 즉 애그리거트 경계가 “도메인 언어로 간주”된다는 뜻이다.

그럼 도메인 엔티티는 어떤 인터페이스를 참조할 수 있는거지?

  • 의미 있는 추상화
    • 도메인 모델에 주입되는 인터페이스는 단순히 테스트 목적으로만 존재해서는 안 됨.
    • 실제 도메인 개념을 반영하여, 여러 구현체가 존재할 수 있는 진정한 추상화를 제공해야 함.
  • 테스트와 설계의 균형
    • 단위 테스트를 쉽게 하기 위해 의존성 주입을 남용하는 것은 설계 상의 문제로 이어질 수 있음.
    • 즉, 테스트를 위한 '해킹'이 아닌, 도메인 자체의 의미와 책임에 부합하는 구조여야 함.
  • 재사용된 추상화의 원칙
    • 좋은 추상화란 실제 비즈니스 요구사항을 반영하며, 최소한 두 개 이상의 구현체가 존재해야 한다는 원칙을 지켜야 함.
    • 일부에서는 최소 세 개 이상의 구현체를 권장하기도 하는데, 이는 추상화가 단순한 목적으로만 사용되지 않도록 하기 위한 기준.

출처: https://enterprisecraftsmanship.com/posts/domain-model-isolation/

참고 2) 근데, 이메일 유일성 검증이 비즈니스 로직으로 가능한가?

  • 불가능하다.
  • 유일성 검증은, 비즈니스 로직만으로는 필연적으로 동시성 문제에 부딛치게 된다.
  • 그렇다고 DB에 검증로직을 온전히 맡겨버리면, 비즈니스 로직이 인프라 로직에 의존하게 되는 문제가 발생한다!

그럼 어떻게 해야 하나?

  1. 리포지토리 구현체에서 유니크 제약조건 예외를 잡는다.
  2. 리포지토리 구현체는 해당 예외를 잡아 비즈니스 로직 커스텀 예외로 바꿔 던진다.
  3. 비즈니스 로직은 해당 예외를 잡아, 비즈니스 로직으로써 처리한다.

참고 3) 그럼 동시성 문제와 같은 것들은, 모두 비즈니스 로직이 인프라 로직에 의존하는 것 아닌가?

그걸 위해, 우리는 애플리케이션 레이어를 사용하는 것이다.
애플리케이션 레이어는, 도메인 로직이 아닌 성능과 데이터 일관성과 같은 비기능 요구사항을 처리하기 위해, DB에 락을 거는 등 인프라 로직을 위한 도메인 레이어의 인터페이스를 호출하게 된다.