brunch

도메인 주도 디자인 (도메인 모델 기반 개발)

Domain Driven Design

by florent

이 글은 Eric Evans의 Domain-Driven Design: Tackling Complexity in the Heart of Software를 번역, 의역, 재구성한 글입니다.


Frame 606.png



[도메인 모델(domain model)이란?]


애플리케이션에 존재하는 대부분의 심각한 복잡성은 기술적인 이유로 발생하지 않는다. 사실, 실제 사용자의 환경, 사용 맥락, 작업 흐름이 고려되지 않고 함부로 만든 후, 후에 되돌릴 수 없음을 뒤늦게 알아차려 고초를 겪는 경우가 빈번하다.


사용자가 목표하는 작업을 유의미하게 수행하는 소프트웨어를 만들기 위해서는, 해당 프로덕트들 담당하는 팀이 그 활동에 관련된 지식을 보유하고 있어야 한다. 활동에 관련된 지식들을 적절히 단순화하고, 사용자의 문제를 해결할 수 있는 흐름으로 설계된 것을 ‘도메인 모델(domain model)’이라고 부른다.



[좋은 도메인 모델은 어떻게 만들어지는가?]


즉, 도메인 모델은 사용자의 활동과 관련된 지식과 정보들을 추상화, 관념화하여 질서정연하게 조직한 체계를 의미한다. 그렇다면 좋은 도메인 모델은 무엇일까? 코드로 쓰면 될까? 다이어그램을 그리면 될까? 분명히 해둘 것은, 위와 같은 코드, 다이어그램 등은 도메인 모델을 표현할 수단일 뿐, 이러한 산출물이 나왔다고 해서 도메인 모델이 잘 만들어진 것이 아니다.


좋은 도메인 모델은 소프트웨어가 만들어진 이유, 사용자가 마주한 비즈니스 환경과 원칙들을 개념적으로 명쾌하게 설명해준다. 이러한 명쾌한 개념이 성립하기 위해선, 해당 비즈니스 도메인의 전문가와 이를 개발하려는 프로덕트팀이 매우 긴밀하게 양방향으로 협력해야 한다. 그렇기에 좋은 도메인 모델은, 프로덕트팀이 사용자의 비즈니스 원칙들을 배우도록 강제하며, 도메인 전문가 또한 자신이 알고 있는 것을 정제하도록 강제한다.


현실의 문제를 탁월하게 해결하는 도메인 모델을 만드려면, (1) 빠른 시각화로 모델을 표현하고, (2) 모델을 기반으로 공동의 언어를 구축하며, (3) 모델에 관련된 행동, 규칙 등을 상세화한 후, (4) 모델 정제하는 과정을 반복하며 모델을 고도화해야한다.


그렇기에 도메인 모델은 문서의 레퍼런스처럼 단순한 수단으로 여겨지지 않는다. 좋은 도메인 모델이 구축된 조직에서는 정보의 흐름이 일방적이지 않고 직무간 활발하게 오고가며, 정보와 지식이 끊임없이 축적된다. 그 과정에서 정보의 위계, 특성, 더 나아가서는 우선순위가 명확해지기 때문에, 자연스럽게 소프트웨어의 복잡성은 단계적으로 해소된다.


일반적인 워터폴 형식의 업무 흐름에서는, 비즈니스의 요구사항을 분석하는 단계와 설계하는 단계가 완전히 분리되곤 한다. 분석하는 단계에서는 비즈니스 전문가들만 알법한 단어들이 난무하게 되고, 그걸 전달받은 개발팀은 최대한 자신이 이해한대로 기술적인 설계를 진행한다. 그렇게 만들어진 소프트웨어는 요구사항을 어정쩡하게 충족하며, 총체적 난국이 시작된다.


도메인 모델에서는 개념적 분석과 소프트웨어 설계를 연결하고 통합한다. 도메인 모델은 그 자체로서 보다 자연스럽게 소프트웨어에 구현될 수 있으며, 도메인에 대한 깊은 지식과 통찰을 전달한다. 또한, 코드는 모델의 표현이 되며, 코드의 변경은 곧 모델 자체의 변경을 의미하는 것이다.



[도메인 모델 구현을 위한 아키텍처]


일반적으로 소프트웨어 시스템의 아키텍처는 프레젠테이션 계층, 애플리케이션 계층, 도메인 계층, 인프라 계층이라는 네 가지 계층으로 나뉜다.


(1) 사용자 인터페이스, 프레젠테이션 계층 (user interface, presentation layer)

사용자에게 정보를 보여주고 사용자의 명령을 해석하는 역할을 수행하며, 사용자는 사람일 수도 있고, 다른 컴퓨터일 수도 있다.


(2) 애플리케이션 계층 (application layer)

소프트웨어가 수행해야 할 작업을 정의하고, 관련된 도메인 객체들이 문제를 해결하도록 지시하는 계층을 의미한다. 이 계층이 담당하는 작업은 비즈니스 측면에서 의미가 있거나, 다른 시스템의 애플리케이션 계층과 상호작용하는 데 필요하다.


이 계층은 얇게 유지되어야 한다. 얕게 유지된다는 것이란, 단순히 작업의 방향과 방식을 조율하며, 애플리케이션 계층 아래에 있는 도메인 계층에 실질적인 작업을 전달하는 역할을 수행하는 것이다. 직접적인 비즈니스 지식이나 규칙이 내포된 계층이 아니며, 사용자나 프로그램을 위한 작업의 진행 상태는 반영할 수 있다.


(3) 도메인 계층, 모델 계층 (domain layer, model layer)

도메인 계층은 소프트웨어의 핵심으로, 사용자의 비즈니스에 관련된 개념, 정보, 규칙 등을 표현하고 이를 작업적으로 내포한 계층을 의미한다. 비즈니스 상황을 반영하는 상태(state)와 작업들은 는 여기서 제어되고 사용되며, 그 저장의 기술적 세부 사항은 인프라 계층에 위임된다.


(4) 인프라 계층 (infrastructure layer)

상위에 존재하는 계층들인 프레젠테이션, 애플리케이션, 도메인 계층을 지원하는 일반적인 기술적 기능들을 제공한다. 이 계층은 또한 네 계층 간의 상호작용 패턴을 아키텍처 프레임워크를 통해 지원할 수도 있다.


도메인 모델과 관련된 모든 코드는 도메인 계층에 집중시키고, 나머지는 각자의 역할에 맞는 코드들을 분배함으로써 도메인 계층이 도메인 모델 표현에만 집중할 수 있도록 해야 한다. 도메인 계층은 다른 관리 책임으로부터 자유롭게 할 수 있게 하고 각 계층이 전문화된 작업을 처리함으로써 설계가 깔끔해질 뿐만 아니라 ,각 계층이 각자의 속도대로 변화하고 반응하기 때문에 유지 비용도 적게 든다. 또한, 이러한 분리는 분산 시스템에서의 배포에서도 크게 도움이 된다. 각 계층을 분리하여 관리하면 통신 오버헤드를 최소화하고 성능을 향상시킬 수 있다.



[도메인 모델 구현을 위한 관계 설정]


도메인 모델에서는 관계를 더 다루기 쉽게 하기 위해 세 가지 방법을 원칙으로 삼는다.

(1) 관계 방향을 지정한다.

(2) 한정자(qualifier)를 추가하여 다중성을 줄인다.

(3) 불필요한 연관성을 최소화한다.


객체간 관계는 가능한 한 제한하는 것이 중요하다. 관계의 방향성을 제한하면 상호 의존성을 줄이고 설계를 단순화할 수 있다. 또한, 이러한 관계적 원칙은 도메인에 대한 이해의 흐름을 확립시키기도 한다. 모델 내 존재하는 객체들의 관계가 한 방향으로 단순화되어 훨씬 쉽고 실용적이게 되며, 혹시나 발생하는 양방향의 관계성이 더더욱 중요한 것임이 부각되기도 한다.



[도메인 모델 구현을 구성하는 요소 1 - 객체(object): 엔티티(entity)와 값 객체(value object)]


(1) 엔티티


엔티티는 도메인 내에서 연속적 정체성을 가지는 객체로, 시간의 흐름과 여러 형태를 거치더라도 본질적으로 동일한 개체로 간주되는 대상이다. 정체성(identity)이 가장 중요한 특징이며, 이는 속성(attribute)이나 현재 상태(state)가 아닌, 존재 자체의 연속성에 의해 정의된다.


엔티티의 구분은 그 객체들이 공유하는 고유한 식별자(ID)나, 도메인에서의 의미 있는 연속성을 기준으로 한다. ID는 직접 부여할 수도 있고, 도메인의 이해를 바탕으로 자연스럽게 정의될 수도 있다. 속성이나 구조가 달라지더라도 같은 정체성을 유지한다면 동일한 엔티티이다. 현실 세계의 예로는 “고객”, “직원”, “거래” 등이 있다. 구현상으로는 ID나 식별자 속성을 통해 관리되며, 이 ID는 시스템 내에서 고유하고 불변해야 한다.


(2) 값 객체

도메인에 대한 설명적 측면을 나타내지만, 개념적 정체성이 없는 객체를 의미한다. 값 객체는 ‘무엇인지(what)’에만 의미가 있는 설계 요소를 표현하기 위해 생성된다. ‘누구인지(who)’ 또는 ‘어느 것인지(which)’는 중요하지 않다.


값 객체는 다른 객체들의 조합체일 수 있으며, 값 객체는 엔티티를 참조할 수도 있다. 집을 짓는 소프트웨어에서 ‘창문 스타일’이라는 객체는 ‘창문의 재료’, ‘높이’, ‘너비’와 같은 다른 값 객체들로 조합되어 나오는 값 객체가 될 수 있다. 또한, 지도 서비스에서 경로 분석을 위해 서울과 부산이라는 지역 엔티티를 참조할 수도 있다.



[도메인 모델 구현을 구성하는 요소 2 - 연산: 서비스(service)]


현실에서는 다양한 주체간 활동과 작업들이 벌어진다. 이를 반영하려면 객체들 사이에서도 유사한 활동이나 작업이 일어나야 하는데, 이러한 상호작용은 객체와 같은 사물로 취급하기에는 어색하다. 이러한 상호작용은 소프트웨어에서 연산이라고 칭하는데, 이러한 연산을 객체에억지로 끼워넣으면 객체의 역할을 흐리고 소프트웨어를 더 복잡하게 만든다.


바로 이러한 상호작용을 ‘서비스’라고 칭한다. 상태를 가지는 엔티티와 값 객체와 달리, 서비스는 모델 내에서 독립적으로 존재하며 상태를 캡슐화하지 않고, 인터페이스 형태로 제공되는 연산이다. 서비스는 기술적 프레임워크에서 흔히 쓰이지만, 도메인 계층에서도 적용될 수 있다.


송금 서비스에서 각 계층의 서비스 역할은 다음과 같다. 애플리케이션 계층의 주요 서비스는 사용자의 입력을 처리하여 도메인 서비스에 메세지를 전송하고 대기하며, 결과에 따라 인프라 계층에 작업을 지시하는 것이다. 도메인 계층의 주요 서비스는 해당 데이터를 받아, 필요한 계좌와의 상호작용, 비용 처리, 결과 처리 응답을 수행한다. 인프라 계층의 주요 서비스는 애플리케이션의 지시대로 알림을 전송하는 등의 작업을 하는 것이다.



[도메인 모델 구현을 구성하는 요소 3 - 개념적으로 연관된 코드의 묶음: 모듈(module)]


모듈은 도메인 모델에서 개념적으로 관련 있는 요소들을 묶어 하나의 단위로 표현하는 고수준의 구성 단위로, 단순히 소스 코드 폴더 구조가 아니라, 도메인의 개념적 이야기와 구조를 담는 설계적 단위로 취급되어야 한다.


모듈은 다음과 같은 원칙으로 만들어져야 한다.

- 개념 중심의 분할: 단순한 기술적 구조(예: UI, DB, 서비스 구분)가 아닌, 도메인 개념을 기준으로 모듈을 구분해야하며, 도메인에서 서로 독립적으로 이해 가능한 개념들 간의 경계를 나타낼 수 있어야 한다.

- 낮은 결합(low coupling): 으로 모듈 간의 상호 의존성을 최소화하여, 하나의 모듈을 이해할 때 다른 모듈을 참조하지 않아도 될 정도로 분리되어야 한다.

- 높은 응집도(high cohesion): 모듈 내부의 클래스와 오브젝트들은 하나의 개념적 이야기나 책임에 집중되어 있어야 하며, 서로 관련 없는 클래스들을 억지로 같은 모듈에 넣어선 안 된다.



[도메인 모델 구현을 구성하는 요소 4 - 도메인 모델 내 일관성을 위한 묶음: 애그리게이트(aggregate)]


애그리게이트(aggregate)는 관련된 객체들의 집합으로, 데이터 변경을 하나의 단위로 다루기 위해 묶은 것이다. 각각의 애그리게이트는 루트(root)와 경계(boundary)를 가진다. 경계는 해당 애그리거트에 무엇이 포함되어 있는지를 정의한다. 루트는 애그리거트에 포함된 하나의 명확한 엔티티이며, 외부 객체가 참조할 수 있는 유일한 구성 요소다.


애그리게이트는 도메인 모델 내에서 하나의 변경 단위로 취급되는 연관된 객체들의 집합이며, 다음의 구성 요소를 지닌다.

- 루트 엔티티(aggregate root): 애그리거트의 진입점이자 대표자로, 외부에서는 오직 이 루트를 통해서만 애그리거트 내부 객체에 접근할 수 있다.

- 경계(Boundary): 애그리거트 내부에 포함되는 객체들의 범위를 정의한다.

- 내부 객체들: 루트와 함께 한 트랜잭션 범위 내에서 변경되고, 루트를 통해 간접적으로만 접근 가능하다.

예를 들어, 차 번호로 구분되는 자동차들의 부품을 추적한다고 할 때, 자동차가 루트트 엔티티이며, 각 부품 자체와 부품들의 위치 등 속성은 내부 객체가 된다. 외부에서 해당 부품들에 대해 조회를 하려면, 자동차를 통해서만 해당 부품들의 정보에 접근할 수 있게 된다.


애그리게이트 설계시 주의사항은 다음과 같다.

- 불변조건(Invariants)의 일관된 적용: 트랜잭션은 반드시 애그리게이트 단위로 처리되어야 하며, 변경 시 전체 애그리거트의 불변조건이 만족되어야 한다. 불변조건은 애그리거트 내부 객체들 간의 관계까지 포함할 수 있다.

- 외부 접근 제한: 외부 객체는 오직 루트 엔티티에만 접근하거나 참조할 수 있다. 내부 객체들은 외부에 직접 노출되어서는 안 되며, 루트를 통해 간접적으로 노출되어야 한다. 따라서, 루트가 제어권을 가지므로, 내부의 무결성이 깨지는 것을 방지할 수 있다.

- 식별성 관리: 루트 엔티티는 전역 식별자(global identity) 를 가진다. 내부 엔티티들은 로컬 식별자(local identity) 를 가지며, 애그리거트 외부에서는 구별될 필요가 없다.

- 삭제 시 전체 제거: 루트가 삭제되면 전체 애그리거트가 함께 삭제되어야 한다.

- 트랜잭션 경계 관리: 애그리거트는 트랜잭션 경계이기도 하다. 하나의 트랜잭션에서는 하나의 애그리거트만 수정하는 것이 원칙이지만, 여러 애그리거트를 걸친 작업은 비동기 메시지나 이벤트 기반으로 설계해야 한다.

- 참조는 루트 간에만: 내부 객체는 다른 애그리게이트의 루트 엔티티만 참조할 수 있다. 서로 다른 애그리거트의 내부 객체끼리는 직접 연결되면 안 된다.



[도메인 모델 구현을 구성하는 요소 5 - 객체 및 애그리게이트를 생성: 팩토리(factory)]


팩토리는 복잡한 객체 또는 애그리게이트를 생성하는 과정을 추상화하고 캡슐화하는 도메인 설계 요소로, 객체 생성과 연결 과정에서 발생하는 복잡성과 내부 구조를 클라이언트로부터 감추는 역할을 한다. 객체 생성 로직 자체는 사용자의 도메인과 직접적인 관련이 없기 때문에, 객체 자체의 책임과 구분되어야 하며, 구현상의 문제로 취급되어야 한다. 따라서 사용자(클라이언트)는 객체의 구체적인 생성 방식이나 내부 구조를 모르더라도 원하는 결과만 지정하면 애그리게이트를 생성할 수 있게 된다. 또한, 내부 결합 규칙을 보호함으로써 내부 구조가 드러나지 않게 보호하는 역할도 수행한다.


팩토리 설계시 주의해야할 점은 다음과 같다.

- 객체 생성을 책임지는 팩토리는 반드시 원자적(atomic)이어야 하며, 불변 조건(invariant)을 보장해야 한다.

- 팩토리는 구체 클래스(concrete class)가 아니라 인터페이스나 타입에 맞게 설계되어야 한다.

- 팩토리가 너무 복잡해지는 경우, 내부 구현 또는 파라미터 선택에 따른 결합도가 높아질 수 있으므로, 가능한 입력 파라미터는 도메인 하위 계층의 객체로 제한해야 한다.

- 값 객체의 경우 팩토리가 모든 속성을 받아 최종 상태로 생성해야 하며, 추가 수정이 없어야 한다.

- 저장된 객체를 재구성하는 경우 연속성을 위해 새 추적 ID를 만들지 않으며, invariant 위반이 발생해도 유연하게 처리해야 한다.



[도메인 모델 구현을 구성하는 요소 6 - 도메인 객체의 생성 이후를 관리: 레포지토리(repository)]


레포지토리는 도메인 객체의 수명 중간부터 끝까지를 관리하는 도메인 계층의 구성요소다. 클라이언트가 도메인 모델의 용어로 객체를 요청할 수 있도록 도와주는 저장소 인터페이스이며, 실제 저장 매커니즘(SQL, 데이터베이스 등)의 복잡성을 숨긴다.


즉, 객체의 저장(persist), 조회(retrieve), 삭제(delete) 등을 도메인 모델 관점에서 다루며, 복잡한 데이터베이스 접근을 감추고, 클라이언트에 의도 중심의 API 제공을 통해 클라이언트가 객체를 직접 생성하거나 조작하지 않고, 모델 개념에 집중하도록 유도한다.


레포지토리 설계시 주의해야할 점은 다음과 같다.

- 팩토리와 역할 분리: 팩토리(Factory)는 새로운 객체를 생성하며, 레포지토리는 기존 객체를 재구성하고 관리한다. 둘을 혼합하면 도메인 경계가 모호해지고 유지보수가 어려워진다. 특히 "find or create" 방식은 피해야 한다.

- 트랜잭션 관리 위임: 레포지토리는 트랜잭션을 직접 커밋하지 말고, 클라이언트(또는 서비스 계층)에 맡기는 것이 좋다.

- 검색 범위 제한: 객체 루트(root) 또는 특별히 필요한 객체만 전역 조회 허용한다.

- Aggregate 내부 객체는 루트를 통해서만 접근하도록 제한해야 불변조건(invariant)을 유지할 수 있다.

- 인터페이스는 모델 용어로 클라이언트는 SQL이 아니라 “모델 개념”으로 레포지토리에 접근해야 한다.

- 유연성 유지: 다양한 검색 조건을 처리할 수 있도록 사양(specification) 기반 조회를 지원하며, 테스트를 위한 인메모리 구현체로 쉽게 교체할 수 있어야 한다.

keyword
작가의 이전글25년 5월 2일 흠터레스팅 테크 뉴스