저번 포스팅에서는 Aggregate에 대해서 알아보았습니다.
지난 포스팅까지의 내용을 요약해보면,
도메인 주도 설계는 Bounded Context 그리고 Aggregate 를 통하여 논리적으로 명확히 구분되는 경계를 통하여
설계를 캡슐화하고 비대해지는 코드에 따른 변경의 파급효과를 줄인다.
라고 할 수 있겠습니다.
이번 포스팅에서는 Aggregate 는 어떤 요소들로 구성되는지 살펴보겠습니다.

Entity 엔티티
엔티티란 식별성으로 정의되는 객체를 말합니다.
책에서는 엔티티를
1. 생명주기 동안 형태와 내용이 바뀌어도 연속성은 유지되는것.
2. 한 객체가 속성보다는 식별성으로 구분되는 것
이라고 정의합니다.
즉 Entity 는 ID(식별자) 가 존재하고, 생명주기동안 속성이 아무리 바뀌어도 식별자가 같으면 같은 객체라고 인식되어야 합니다.
우리는 설계를 할때 항상 이야기하는 '변경'에 가장 집중해야합니다. Entity 의 속성은 변경의 여지가 많지만, 식별자는 그렇지 않습니다.
Entity 는 식별성이 최 우선적으로 고려되어야 한다는 것입니다.
책에서는 그로인해 Entity 를 도출하는 몇가지 방법을 소개합니다.
엔티티의 도출
- 엔티티는 속성이나 행위에 집중하기 보다는, 본질적인 특징 즉, 식별하고 탐색해서 일치시키는 것만으로 정의해야한다.
- 어떤 것이 같은 것으로 분류되어야하고, 어떤 것이 다른것으로 분류되어서 연속성을 지녀야하는지가 최 우선적으로 고려되어야 합니다.
- 개념에 필수적인 행위만 추가하고 그 행위에 필요한 속성만 추가한다.
- 식별성과 연속성이 고려되었다면, 행위 단위로 책임을 분리하고 속성을 부여합니다.
- 그 밖의 객체는 행위와 속성을 검토해서 가장 중심이 되는 ENTITY와 연관관계에 있는 다른 ENTITY 혹은 VALUE OBJECT 등에 옮긴다.
- 분리된 행위와 속성을 바탕으로, 한번 더 다중으로 책임이 부여되지 않았나 확인하고 분리하여 캡슐화합니다.
- 식별성 문제를 제외하면 ENTITY는 주로 자신이 소유한 객체의 연산을 조율해서 책임을 완수한다.
- 최대한 캡슐화된 내용을 바탕으로 부여한 책임에 대한 행위를 구현합니다
책에 나온 원문 그대로와 제가 해석한 부분을 분리해서 기술해보았습니다.
결국 DDD 는 객체지향을 바탕으로 한 설계이기 때문에 SOLID, GRASP 등의 객체지향 설계 원칙들을 따르기 위한 방법이 가이드되어 있다고 보입니다.
Value Obejct (값 객체)
엔티티처럼 개념적인 식별성을 지니는 객체도 있지만, 그렇지 않은 객체들도 많습니다.
그 중에서도 Value Object는 사물의 어떤 특징을 묘사합니다.
엔티티와는 다르게 값 객체는 이름 처럼 식별성에는 관심사를 두지 않고, 해당 요소가 무엇인지에 대해서만 관심사를 부여합니다.
값 객체는 단순히 설명으로는 이해가 잘 되지 않을 것 같아 예시를 하나 들어보겠습니다.
class Point(
val x: Int,
val y: Int
)
예를 들어 위처럼 2차 좌표계의 어떤 특정 위치를 나타내는 좌표를 코드로 표현하고 싶습니다.
만약 x 좌표가 50 y 좌표가 50 인 Point 인스턴스가 두개 생성되었을때, 이 둘은 다른것으로 식별성을 주어야할까요?
대답은 No, 같은 좌표 위치이므로 따로 식별될 필요가 없습니다.
이처럼 식별성은 고려 대상이 아닌 어떠한 값들을 투영하여 객체로 표현한 것이 값 객체입니다.
그렇다면 코드상에서도 그러한 성격이 구현되어야 하기 때문에,
속성이 모두 같다면 동일성을 만족하도록 equals() & hashcode() 를 구현하여야 합니다.
또 한가지 특성이 더 있습니다. 한번 인스턴스화 된 객체의 값이 변경이 된다면?
속성이 달라지기 때문에 전혀 다른것으로 식별될 것입니다. 이러한 일을 방지하기 위하여
값 객체는 불변으로 구현되어야 합니다.
다시 엔티티 처럼 책에서 말하는 Value Object 를 구현하는 방법을 살펴봅시다.
Value Object 의 도출
- 모델에 포함된 어떤 요소의 속성에만 관심이 있다면 그것을 VALUE OBJECT로 분류하라.
- VALUE OBJECT에서 해당 VALUE OBJECT가 전하는 속성의 의미를 표현하게 하고 관련 기능을 부여하라.
- VALUE OBJECT는 불변적으로 다뤄라.
- VALUE OBJECT에 는 아무런 식별성도 부여하지 말고 ENTITY를 유지하는 데 필요한 설계상의 복잡성을 피하라.
위 예제에서 충분히 다루었다 생각하고 상세히 후술하지는 않겠습니다.
Factory 팩토리
객체의 생성은 생성하는 객체의 내부구조를 많이 알고 있어야 하는 작업입니다.
객체들에게 객체 생성의 책임까지 맡기는 것은 결합도를 높이고, 시스템을 복잡하게 만들 가능성이 있습니다.
Aggregate 를 생성하는 일이 복잡해지거나, 생성의 책임에서 내부 구조가 많이 드러나는 경우 Factory 를 이용합니다.
즉 FACTORY 는 객체 생성에 대한 지식을 캡슐화하고 객체 생성에 대한 책임을 위임받은 프로그램 요소 입니다.
Factory 역시 책에서 2가지 지켜야하는 룰을 소개합니다.
Factory 규칙
- 각 생성방법은 원자적이어야하며, AGGREGATE 의 불변식을 지켜야한다. ENTITY의 경우는 AGGRAGATE 전체의 불변식을 말하며, VALUE OBJECT 는 모든 속성이 완전함을 이야기한다. 올바르게 생성하지 못하는 객체에 대해서는 예외를 발생시켜라
- 팩토리에만 해당되는 이야기는 아닙니다. 각각의 요소들은 생성될때 부터 최대한 완전한 상태로 생성되어야 하며 Aggregate의 관점에서 보면 고유한 생명주기에 맞게 생성이 되어야 합니다, Value Object 에 대해서는 모든 값이 채워진 상태로 생성이 되어야 합니다.
- FACTORY는 생성된 클래스보다는 생성하고자 하는 타입으로 추상화 되어야한다.
- 흔히 알고 있는 DIP 원칙을 이야기합니다. 항상 구체화가 아닌 추상화에 의존하도록하여 의존성을 끊어냅니다.
Factory 의 위치
Factory 는 흔히 말하는 Factory Method Pattern 의 팩토리 같은 생성만을 담당하는 별도의 클래스도 있지만, 넓은 범위에서는 생성자를 제외한 객체를 생성하는 여러 방법을 포괄 합니다.
- AGGREGATE ROOT 에 BOUNDARY 내부 객체에 대한 FACTORY METHOD 를 생성
- Aggregate Boundary 의 루트를 제외한 모든 요소는 루트를 통하여 접근되어야 합니다. 즉 어그리거트 내에서 가장 생성될 객체의 정보를 많이 아는 것은 Root 일 가능성이 높습니다. 또한 루트가 생성되는 객체를 필드로서 소유하게 되는 경우, 아예 외부에 생성되는 객체가 드러나지 않는다는 점에서 완전한 캡슐화를 이뤄낼 수 있습니다.
- 객체를 소유하진 않지만 객체를 만들어내는 것과 밀접한 책임이 있는 객체에 FACTORY METHOD
- 생성될 객체를 소유하진 않지만, 논리적으로 해당 객체를 생성되는 책임이 큰 클래스가 있을 경우가 있습니다. 보통 객체의 책임할당은 GRASP 패턴을 통하여 해당 책임을 파악해보면 좋을것 같습니다.
- 독립적인 FACTORY 분리
- 적절한 책임이 있는 클래스에게 Factory 메서드를 부여함으로서, 책임이 과도해지거나 생성의 책임을 맡을 적절한 객체가 없을 경우, 객체 생성의 책임만 할당받는 별도의 클래스로 분리하는 방법이 있습니다.
하지만 모든 객체를 Factory Method 나 클래스로 생성하는것이 항상 좋지는 않습니다.
생성자로 충분할 경우 그냥 공개 생성자를 제공하는 것이 더 복잡성을 낮추는 바람직한 방법입니다.
생성자가 더 나은 경우
- 클래스가 타입인 경우, 즉 다형적으로 사용되는 타입이 아닌경우.
- 클라이언트가 STRATEGY를 선택하는 방법으로서, 구현체에 관심이 있는 경우
- 클라이언트가 객체 속성을 모두 이용할 수 있어서 생성의 책임이 클라이언트에게 추가적인 정보를 제공하지 않는 경우
- 생성자가 복잡하지 않은 경우
즉 생성되는 객체가 다형성을 활용하지 않거나, 캡슐화할 필요가 없을 경우, 생성자 그 자체가 로직이 되는 경우에는 Factory 를 고려해보아야 합니다.
그럼 Factory 를 생성하는데 있어 주의할 점은 어떤 것이 있을까요?
Factory 의 인터페이스 설계
- 각 연산은 원자적이어야한다. 불변식을 위배하는 객체를 생성하는 경우 예외나 반환 값에 대한 표준을 도입하라.
- Aggregate 혹은 객체 스스로의 불변식을 모두 만족하는 완전한 객체를 생성하거나, 아예 생성하지 않거나 하나만 하라는 뜻입니다. 불변식을 위배하는 객체가 생성이 되면 안됩니다.
- FACTORY 는 전달받은 객체와 결합한다. 인자가 단순히 생성물에 들어가는 것이 가장 의존성이 적당한 상태이며, 인자를 끄집어내어 생성 과정에 이용한다면 결합이 더 강해질것이다.
- 모든 메서드가 그렇듯, 전달받은 인자와는 결합도가 생깁니다. Factory 의 인자와의 결합도는 생성에 바로 인자를 사용하는 것이 적당하고, 그 인자를 통해서 연산을 해 생성하는 것을 지양하라고 제시합니다.
즉, 생성에 가장 밀접하고 가장 하위 수준의 객체 그리고 추상적인 타입 등을 매개변수로 우선 고려하여 결합도를 낮추는게 좋겠습니다.
- 모든 메서드가 그렇듯, 전달받은 인자와는 결합도가 생깁니다. Factory 의 인자와의 결합도는 생성에 바로 인자를 사용하는 것이 적당하고, 그 인자를 통해서 연산을 해 생성하는 것을 지양하라고 제시합니다.
재구성 Factory
보통 일반적인 어플리케이션은 영속화할 저장소와 함께 구동됩니다. 구현하기는 어렵지만 가장 이상적인 형태의 DDD 모델은 영속화 모델과, 도메인 모델을 구분합니다. 자세한 구현은 나중에 구현해본 후기 포스팅을 할 여유가 된다면 작성해보겠습니다.
영속화 모델과 도메인 모델을 구분함을 떠나서, 일반적으로 객체가 영속화 될때
객체의 특성이 그대로 유지되는 경우는 거의 없습니다. RDBMS 를 예로들면, 일반적으로 합성되어 있는 객체들을 flat 한 형태로 저장합니다.
재구성 Factory 란, 영속화를 하거나 영속화된 데이터를 객체로 가져올 때, 사용하고자 하는 객체의 구조에 맞게 재구성하는 요소를 말합니다.
재구성 Factory 는 나중에 구현 코드와 함께 보면 이해가 쉬울 것 같습니다.
재구성 Factory 는 Factory 의 규칙을 대부분 만족하면서 아래의 2가지 조건을 더 만족해야합니다.
- 재구성 FACTORY는 새 ID를 할당하지 않는다.
- 너무 당연한 이야기입니다. 식별자를 수정하지 않습니다.
- 불변식 위반을 단순히 객체를 생성하지 않는 것이 아닌 더 탄력적인 방법으로 처리해야한다.
- 단순히 예외를 뱉고 넘어가는 일반적인 Factory 와는 달리, 어떠한 기준을 두고 불변식을 만족시키도록 추가적인 연산을 한다던지 하는 유연한 처리가 필요합니다.
Service 서비스
개발을 하다보면, 명확히 실용적이고 필요한 부분이 있지만 책임 할당이 모호한 기능들이 생깁니다.
이러한 연산들을 우겨넣기 보다는 명확하게 책임을 분리하여 처리하기 위한 설계 요소를 Service 라고 합니다.
특징
- 연산이 원래부터 엔티티나 값 객체의 일부를 구성하는 것이 아니라 도메인 개념과 관련돼 있다.
- 인터페이스가 도메인 모델의 외적 요소의 측면에서 정의된다.
- 어렵게 설명이 되어 있지만, 말 그대로 값 객체와 엔티티 레벨에서 해당 기능을 수행하기에는 책임 할당도 모호하고, 애매하지만 명확히 도메인 레벨의 기능인 것들의 집합이라는 뜻입니다.
- 연산이 상태를 갖지않는다.
- Service 는 Stateless 해야합니다. 일반적으로 속성을 지니지 않으며, 어플리케이션에서 싱글톤으로 존재하고 별도로 추가적인 인스턴스를 생성하지 않습니다. 왜냐하면 서비스는 식별성이 최우선적으로 고려된 '엔티티' 가 아니기 때문입니다. 서비스는 도메인 로직 혹은 클라이언트의 요구사항을 투영한 '기능' 의 집합체이지 어떠한 속성을 가지고 연속성을 지니는 개념이 아닙니다.
분류
서비스는 크게 3가지로 분류됩니다.
- 인프라스트럭쳐 서비스
- 응용 서비스
- 도메인 서비스
https://dev-sj-repo.tistory.com/16 해당 포스팅을 같이보면 이해하기 쉬울것 같습니다.
인프라스트럭쳐 서비스
인프라스트럭쳐 서비스는 외부 프레임워크나 라이브러리 혹은 서버등 도메인이 아닌 외부의 것들과 의존관계가 있는 서비스입니다.
응용 서비스
응용 서비스는 인프라스트럭쳐 코드(비단 서비스 뿐만이 아닌 모든 인프라스트럭쳐 레이어를 이야기합니다) 와 도메인 레이어를 연결하는
Facade(창구) 역할을 합니다. 전역적인 규칙을 포함하거나, 클라이언트의 요구사항을 여러 Aggregate 끼리의 조합하여 처리합니다. 일반적으로 트랜잭션이 구현되는 위치입니다.
도메인 서비스
위에서 설명한 특징과 가장 들어맞는 서비스입니다. 도메인 로직이 분명하나, 어느 특정 객체에게 할당하기는 모호한 기능들을 도메인 서비스로 응집시킵니다. 하지만 도메인 객체의 응집도를 떨어트리는 행위이므로 정말 도메인 서비스가 필요한지 항상 숙고하여 추가하여야합니다.
금액을 이체하고, 완료될 시 처리 결과를 메일로 발송하는 기능을 Rest API 로 구현 한다고 가정해봅시다.
그럼 각 서비스에는 다음과 같이 기능이 분배될 것입니다.
인프라스트럭쳐 서비스 - 메일 발송 서비스와 연동되는 기능을 구현
도메인 서비스 - 도메인 내부에 이체 기능을 구현하거나, 마땅히 책임을 할당하기 모호하면 도메인 서비스를 두어 해당 기능을 구현
응용 서비스 - API 요청을 받아들이는 adaptor 에서 주는 요청을 받아서 도메인 서비스에 전달하고, 그 결과를 받아 인프라스트럭쳐 서비스에 메일발송을 요청.
Repository 리포지토리
영속화를 배제하고 어플리케이션을 구현하면, 객체는 아마 각 객체에 접근할 방법이 필요해서 모든 객체 탐색그래프가 연결되어 있을것입니다. Aggregate 포스팅에서 객체 그래프 탐색을 제한해야하고 그 탐색에 대한 경계를 논리적이고 직관적으로 나눈것이 Aggregate 라고 했는데 그렇게 되면 아무 의미가 없어지겠지요.
영속화를 함으로서, 우리는 바로 어떤 데이터에 접근할 때 식별자 만으로 직접 접근이 가능해집니다.
즉 객체가 모든 자식 객체들을 컬렉션으로 보유할 필요가 없어지는 것 입니다.
영속화된 데이터에 접근하는 Repository 에 몇가지 규칙을 둠으로서 Aggregate 의 경계를 견고히 할 수 있습니다.
규칙
1. Repository 는 Interface 로 구현하며 영속화 저장소의 대상이 누군지 숨겨, 언제든지 교체가 가능하게 구현하여 확장성 있고 테스트가 용이하게 만듭니다.
2. 영속화된 객체의 접근을 명확히 제한해야합니다. Repository 는 Aggregate 당 하나. 즉 Aggregate Root 에 대해서만 제공합니다.
3. 영속화를 특정 클래스에 대해서만 담당할 필요가 없습니다. "타입" 을 기준으로 두어도 됩니다.
이로 인해, DB 에서 데이터를 꺼내올 때에도 어그리거트 루트를 기준으로만 접근할 수 있도록 명확히 제한이 되었습니다.
저는 도메인 주도 설계를 읽으면서 설명이 너무 어렵게 되어있고 지루한 감이 있었습니다.
처음 읽을때에는 이해가 되지 않던 내용이 실무에서 적용하면서 다른 구현 위주의 책들을 함께 보면서
이해가 가고 각각의 요소의 룰을 지키는 것에 대한 필요성을 느끼게 되었고,
제가 어려웠던 부분을 조금 풀어보고 나눠보고자 해당 포스팅을 작성해보았는데 부족한 필력에 다 담기지 않아 아쉽습니다 ㅎㅎ.
기회가 된다면 실무에서 적용하면서 겪었던 이해부족으로 인한 시행착오와 개선방법에 대해서도 구현과 함께 작성해보겠습니다.
부족하거나 잘못 이해한 내용이 있으면 댓글 달아주시면 감사하겠습니다!
'Architect' 카테고리의 다른 글
| Hexagonal Architecture (헥사고날 아키텍쳐) 란? (2) | 2022.03.14 |
|---|---|
| [DDD] Aggregate 어그리거트 (0) | 2022.02.02 |
| [DDD] BoundedContext 바운디드 컨텍스트 (0) | 2022.02.02 |