도메인은 비즈니스 문제 영역이다. 비즈니스 문제 영역이라 함은 주로 해결하고자 하는 영역의 경계로도 볼 수 있다. ddd는 business problem driven design으로 바꿔 읽어도 된다.

서브 도메인은 도메인과 거의 비슷한 의미로 사용해도 되지만, 더 큰 도메인의 하위라는 점을 강조하고 싶을 때 서브 도메인이라 표현하면 된다.

유비쿼터스 언어

유비쿼터스 언어란 도메인 전문가와 개발자의 언어가 겹치는 부분을 의미한다. 팀 내에서 공유하는, 해당 팀에서 정의한 언어. 이건 단순히 도메인 전문가와 소통할때만 사용하는 것이 아니라, 코드에서도 사용할 수 (해야)한다. 팀에서 이 유비쿼터스 언어를 주기적으로 관리하고 검토해야 한다(스프린트가 끝날 때 까지 마다) 하나의 유비쿼터스 언어를 회사 전체에 적용하려하면 실패한다, 유비쿼터스 언어는 하나의 bounded context 안에서만 유효하다. 유비쿼터스 언어에는 도메인이 담고 있는 개념을 언어와 코드로 나타낼 수 있어야 한다. 도메인에 없는 개념은 유비쿼터스 언어로 나타내선 안된다. 코드(유비쿼터스 언어)가 비즈니스 규칙을 그대로 표현할 수 있으면 전문가와 같은 단어로 설계를 논의할 수 있다. +우리 회사에선 어떻게 적용해볼 수 있을까.

Bounded Context(이하 B.C)란 큰 모델을 이해하기 쉬운 작은 덩어리로 나누고, 그 덩어리 사이의 관계를 명시적으로 정하는 것. 한 컨텍스트에서 정의한 용어가 다른 컨텍스트에서 동일한 뜻일 필요는 없다. 마케팅 팀과 구독 팀의 campaign은 각각 다른 모델이고, 그렇기 때문에 둘 사이에는 매핑이 필요하다.

ACL: Anti Corruption layer

서로 다른 모델과 요구사항을 가진 두 시스템(업스트림과 다운스트림) 사이에 위치하여, 외부 시스템의 복잡하고 오염된 모델이 내부의 순수한 도메인 모델을 침투하지 못하도록 보호하는 번역(Translation) 계층. ACL은 보통 다음과 같은 4가지 요소로 구성됩니다:

  • Service (서비스): 외부 시스템이나 레거시 API와 직접 통신하는 인터페이스입니다.
  • Adapter (어댑터): 서로 다른 프로토콜, 기술 스택, 데이터 형식을 연결합니다.
  • Translator (번역기): 업스트림(외부) 모델의 개념과 데이터 구조를 다운스트림(내부) 모델로 양방향 변환합니다.
  • Facade (퍼사드): 복잡한 외부 서브시스템의 인터페이스를 단순화하여 내부 시스템이 사용하기 쉽게 만듭니다

엔티티

엔티티는 식별자(id)로 정의할 수 있는, 고유성을 가지는 객체를 의미한다. 주소, 이메일, 성별(?)은 변경될 수 있지만 id가 같다면 동일한 객체로 봐야한다. 광고 상품의 타이틀이나, 단가가 변경되더라도 여전히 동일한 광고 상품으로 봐야한다. (물론 변경하기 애매한 속성이 변경될 땐 차라리 새로운 광고 상품을 만드는게 낫다) 엔티티를 고유하게 만드는 속성이 ID다 보니 DB 테이블 설계가 도메인 모델을 결정해버리는 함정에 빠지기 쉽다. public getter/setter만 잔뜩 있고, 비즈니스 로직은 없는 상태는 엔티티라 부르기 어렵다. 이런 경우에는 비즈니스 로직을 엔티티를 호출하는 쪽에서 각자 구현해버리는데, DDD 측면에서 도메인 전문가가 의도하지 않은 각자의 방식으로 조금씩 다른 구현이 생겨 로직이 흩어지는 문제가 생길 수 있다.

엔티티에 포함하기 적절하지 않은 메타데이터, (가령 트레이싱 ID, 광고 효과, 상품 조회 수 등)는 분리해야 하기 때문이다. 유용한 정보지만 도메인 모델에 포함할 필요는 없다. 가령 CPC 광고 상품을 엔티티로 관리할때, 이 상품의 impression 수치를 도메인 모델에 포함하는게 적절할까? 물론 이 상품의 고유 정보니까 도메인 모델에 포함해야 한다고 볼 수도 있지만, impression을 일별/주간별로 나누어 보고 싶은 니즈를 챙기려면 별도 데이터로 관리하는게 좋다. 이런 데이터는 도메인 모델에 포함할 필요가 없는 셈이다.

-> DB 모델과 도메인 모델을 분리하는 이유는 무엇일까? 도메인 모델은 비즈니스 규칙을 표현하고, DB 모델은 저장·조회·운영을 위해 존재한다. 시스템이 커지면 트레이싱 ID, 광고 효과, 조회 수처럼 유용하지만 도메인 규칙에는 쓰이지 않는 메타데이터가 늘어나는데, 이를 도메인 모델에 넣으면 모델이 비대해지고 의미가 흐려진다. 반대로 DB 스키마에 맞춰 도메인 모델을 만들면 로직 없는 빈약한 모델이 되기 쉽다. 그래서 둘을 분리하고 어댑터 레이어로 변환한다.

판단 기준은 "이 바운디드 컨텍스트의 규칙이 그 데이터를 쓰는가"다. CPM 광고 상품의 과금 컨텍스트에서는 impression이 과금 금액과 예산 소진을 결정하므로 도메인 개념이다. 다만 도메인 모델에는 규칙에 필요한 형태, 즉 누적 노출 수와 그로부터 계산되는 소진 금액만 둔다. "소진 금액은 예산을 넘을 수 없다", "예산을 다 쓰면 노출을 중단한다" 같은 불변식을 엔티티가 직접 구현하게 만든다.

반면 노출 한 건 한 건의 로그(시각, 지면, 기기, 트레이싱 ID)나 일별/주간 집계는 규칙에 쓰이지 않고 리포팅을 위한 것이므로 도메인 모델 밖에서 별도 데이터로 관리한다. 즉 impression이라는 같은 개념도 과금 규칙용 값은 도메인 모델에, 분석·조회용 기록은 DB/리포팅 모델에 둔다.

VO (value object)

엔티티와 달리 식별자가 없고, 값이 같으면 동일한 것으로 취급하는 객체를 의미한다. 엔티티와 달리 도메인의 무언가를 측정, 수량화, 묘사할 때 사용한다(e.g 좌표, 금액, 주소) 엔티티 vs 값 객체 선택 기준은 아래 세 개의 질문에 yes라면 값 객체를 사용하면 된다.

  • 이 객체를 불변으로 다룰 수 있는가?
  • 도메인 개념을 측정·수량화·묘사하는가?
  • 같은 타입끼리 값으로 비교할 수 있는가?

불변으로 다룰 수 있는가라는 질문이 헷갈릴 수 있는데, 생성한 뒤 변경하지 않을 객체를 값 객체로 만들어야 한다. 바꾸고 싶다면 수정이 아니라 새로운 값 객체로 갈아 끼워야 한다. 저자는 일단 값 객체로 시작하고, 맞지 않을 때 엔티티로 승격하라고 한다. 값 객체는 소비자가 의도치 않게 수정할 걱정이 없어 안전하기 때문이다. 근데 AI시대에도 이게 통할지 모르겠다.

Aggregate

특정 행위에 대해 하나처럼 다뤄지는 도메인 객체들의 묶음을 의미한다. 애그리거트의 본질은 트랜잭션 경계를 의미한다. 바운디드 컨텍스트의 불변식을 찾아야 한다. (불변식은 도메인에서 항상 참이어야 하는 규칙을 의미). 애그리거트는 즉시적, 원자적 일관성이 필요한 범위를 의미한다. 한 트랜잭션에서는 애그리거트 하나만 수정해야 한다. 하나의 트랜잭션에서 여러 애그리거트를 건드려야 한다면 모델을 다시 확인해봐야 한다. (근데 코드를 안읽는데 이게 무슨 의미가 있나). 애그리거트를 설계할 땐 작게 시작해야한다. 작은 범위일 수록 확장성과 성능이 좋고 트랜잭션 성공 확률이 높아진다.

Factory

객체를 생성하는 것이 주 책임인 객체or함수를 의미한다. Go는 객체지향 언어가 아니지만 BuildCar(type) 같은 함수 형태로 똑같이 사용합니다. 팩토리를 사용하는 이유는 생성을 표준화 하기 위함이다. 복잡한 struct를 만드는 방법을 한곳으로 통일할 수 있고, 객체 생성에 필요한 default값도 여기서 공통으로 관리할 수 있다. 또한 엔티티는 고유의 비즈니스 로직을 내재화 하는데, 잘못된 값으로 객체가 생성되는 것을 방지할 수 있는 측면에서도 팩토리가 유용하다.

Repository

데이터 소스에 접근하는 로직을 담는 레이어로, 여기에 데이터 접근 코드를 모두 모아두면 유지보수가 편해진다. 특정 DB 기술과의 결합을 끊을 수 있다.

Service

세 종류의 서비스가 있다.(도메인 서비스 / 애플리케이션 서비스 / 인프라 서비스). 내가 매번 헷갈린게 이 지점인듯.

도메인 서비스는 엔티티나 VO 어디에도 자연스럽게 두기 어려운(애매한) 로직을 위한 곳이다. 중요한 비즈니스 로직을 작성 할 때, 한 도메인 객체를 다른 도메인 객체로 변환하거나, 두 개 이상의 도메인 객체 속성으로 값을 계산해야 하는 경우에 도메인 서비스를 사용할 수 있다. 즉 하나의 도메인 내에서만 처리하기 어려운 경우 도메인 서비스를 사용하면 된다.

애플리케이션 서비스는 다른 서비스와 레포지토리를 조합하고, 여러 모델 간 트랜잭션 보장을 관리한다. 도메인 로직은 여기에 두면 안된다. 이 서비스는 아주 얇게, 조율의 역할을 하도록 남기고, 나머지는 아래 레이어로 내린다.

인프라 서비스는 결제(Stripe, PayPal), 이메일(SES)처럼 핵심 도메인은 아니지만 앱에 필요한 기능을 두면 된다. EmailSender같은 인터페이스를 두고, 애플리케이션 서비스나 도메인 서비스에 주입해서 사용하면 깔끔히 격리하여 사용할 수 있다.