[도메인 주도 설계] (4) 기본 구성 요소 1 – 객체

도메인 주도 설계의 첫 기본 구성 요소는 객체(Object)에요. 객체는 엔티티와 값 객체로 나뉘어요. 엔티티는 시간이 지나도 같은 것, 값 객체는 바뀌면 다른 것이에요. 이 근본적 구분이 제품의 복잡성을 줄일 수 있는지 알아보세요.

'Domain-Driven DESIGN'이라는 컬러 타이포그래피 로고. 하늘색 배경에 '도메인 주도 설계 — (4) 기본 구성 요소 1 - 객체'라는 텍스트가 적혀 있다.

지난 편에서 도메인 주도 설계의 계층 구조와 각 계층의 역할을 살펴봤어요. 이번 편에서는 도메인 모델을 구성하는 기본 구성 요소(Building Blocks)의 전체 그림을 보고, 그중 가장 기본이 되는 엔티티(Entity)값 객체(Value Object)의 차이를 구체적인 예시와 함께 알아볼게요.


1. DDD 기본 구성 요소: 도메인 모델의 핵심 개념 지도

도메인 주도 설계는 하나의 기법이 아니에요. 각각 다른 질문에 답하는 기본 구성 요소들의 집합이에요.

핵심 기본 구성 요소를 정리하면 다음과 같아요.

빌딩 블록 나타내는 것 핵심 책임 핵심 질문
객체(Object) 도메인의 의미 있는 개념 도메인의 의미와 행동을 나타냄 “이것이 사용자가 실제로 생각하는 개념인가?”
서비스(Service) 여러 개념에 걸친 비즈니스 결정 객체 간 비즈니스 규칙을 강제 “이 규칙은 하나의 객체에 속하지 않는데, 어디에 두어야 하는가?”
모듈(Module) 도메인 이야기의 일관된 부분 관련 개념을 그룹화 “이 부분은 어떤 종류의 제품 문제를 해결하기 위해 존재하는가?”
애그리게이트(Aggregate) 일관성 경계 불변 조건이 함께 유지되도록 보장 “부분적으로만 업데이트되면 절대 안 되는 것이 무엇인가?”
팩토리(Factory) 도메인 객체의 생성 로직 생성 시점의 규칙을 강제 “이것이 애초에 존재할 수 있는 조건이 무엇인가?”
리포지토리(Repository) 기존 도메인 객체에 대한 접근 조회와 영속화를 관리 “시간이 지나면서 이것을 어떻게 안전하게 다루는가?”

각 빌딩 블록은 서로 다른 종류의 제품 질문에 답하기 위해 존재해요.

이 빌딩 블록들은 제품이 정체성, 변화, 일관성, 확장에 대해 더 어려운 질문에 답하기 시작하면서 자연스럽게 등장해요.

기능이 간단해 보이는데 예상외로 비용이 클 때, 현재 모델에서 어떤 책임이 빠져 있거나, 과부하되었거나, 잘못 배치된 경우가 많아요.


2. “객체”란 무엇인가

엔티티와 값 객체를 다루기 전에, “객체(Object)”가 무엇을 의미하는지 명확히 정의 해야 해요.

도메인 주도 설계에서 객체는 기술적 구조물이나 프로그래밍 산출물이 아니에요. 제품이 추론해야 하는 도메인의 개념을 나타내는 방식이에요.

객체는 이런 역할을 해요.

중요한 건 객체가 어떻게 구현되는지가 아니라, 제품과 관련된 사용자의 현실을 표현하는 데 어떤 역할을 하는지예요.

이 관점에서 DDD는 한 단계 더 구분해요.

이 차이는 기술적인 것이 아니에요. 제품이 변화를 어떻게 다루는지에 관한 것이죠.

객체의 두 가지 종류를 비유하면, 사람과 주소의 관계와 같아요. “김민수”라는 사람(엔티티)은 이사를 해도 같은 사람이에요. 하지만 “서울시 강남구 역삼동 123″이라는 주소(값 객체)는 바뀌면 그냥 다른 주소이지, “같은 주소가 변한 것”이 아니에요.


3. 엔티티 vs 값 객체

계층 구조가 로직이 어디에 위치하는지를 설명한다면, 기본 구성 요소는 그 로직이 무엇으로 구성되는지를 설명해요. 대부분의 도메인 모델이 흔들리기 시작하는 건 팀이 하나의 근본적 구분을 명확히 하지 못할 때예요.

무엇을 엔티티(Entity)로 모델링하고, 무엇을 값 객체(Value Object)로 모델링해야 하는가?

1) 엔티티란 무엇인가: 정체성과 시간에 걸친 연속성

엔티티(Entity)는 세부 사항이 변해도 제품이 시간에 걸쳐 같은 것으로 다루는 무언가를 나타내요. 엔티티를 정의하는 건 특정 순간에 어떻게 보이는지가 아니라, 시스템이 그것의 연속성을 신경 쓴다는 사실이에요.

실무에서 엔티티는 거의 항상 ID를 가져요. 하지만 중요한 아이디어는 ID 자체가 아니라 정체성이에요.

B2B 채용 플랫폼을 생각해 보세요. 채용팀이 내부적으로 지원서와 면접을 관리하는 제품이에요.

대부분의 채용 도메인에서 두 엔티티가 거의 즉시 나타나요.

둘 다 엔티티인 이유는 제품이 변경이 일어난 후에도 같은 것으로 다시 참조해야 하기 때문이에요.

지원서(Application) 엔티티를 시각화하면 이래요.

핵심 개념 (채용 도메인)
────────────────────────────

┌──────────────────────────┐
│     Candidate (엔티티)     │
└─────────────┬────────────┘
              │ 참여
              ▼
┌────────────────────────────────────────────┐
│              Application (엔티티)            │
│--------------------------------------------│
│ 식별자: applicationId                        │
│                                            │
│ 필드:                                       │
│  - candidateId: CandidateId                │
│  - status: ApplicationStatus               │
│  - interviewSlot: InterviewSlot | null     │
│                                            │
│ 불변 조건:                                   │
│  - 상태 전환은 유효해야 함                      │
│  - INTERVIEW 상태일 때 interviewSlot 필수     │
│  - 그 외 상태에서는 interviewSlot null         │
└────────────────────────────────────────────┘

핵심은 단계 자체가 아니라, 정체성이 일정하게 유지되면서 상태가 진화한다는 사실이에요.

사용자가 내일도 여전히 같은 객체로 남아 있을 것을 기대한다면, 엔티티를 다루고 있을 가능성이 높아요.

엔티티를 비유하면, 여권과 같아요. 여권 사진을 바꾸고, 주소를 변경하고, 비자 스탬프가 추가되어도, 여권 번호가 같으면 같은 사람의 같은 여권이에요. 세부 사항이 계속 바뀌어도 정체성은 유지되죠.

2) 값 객체란 무엇인가: 교체 가능한 묘사와 불변성

값 객체(Value Object)는 제품이 무언가를 묘사하기 위해 사용하는 것이지, 무언가를 추적하기 위한 게 아니에요.

구분하는 가장 쉬운 방법은 간단한 질문을 던지는 거예요.

이것이 바뀌면, 여전히 같은 것으로 생각하는가?

답이 아니오라면, 값 객체일 가능성이 높아요.

같은 내부 채용 관리 플랫폼을 생각해 보세요. 후보자(Candidate)와 지원서(Application)가 엔티티인 반면, 면접 슬롯(InterviewSlot)은 매우 다른 역할을 해요. 면접이 언제 예정되어 있는지를 단순히 묘사할 뿐이죠.

이 구분은 InterviewSlot이 도메인의 다른 개념들과 어떻게 관계를 맺는지를 보면 명확해져요.

핵심 개념 (채용 도메인)
────────────────────────────

┌──────────────────────────┐
│     Candidate (엔티티)     │
└─────────────┬────────────┘
              │ 참여
              ▼
┌────────────────────────────────────────────┐
│              Application (엔티티)            │
│--------------------------------------------│
│ 식별자: applicationId                        │
│                                            │
│ 필드:                                       │
│  - candidateId                             │
│  - status                                  │
│  - interviewSlot                           │
│                                            │
│ 규칙 / 불변 조건:                             │
│  - 상태는 허용된 전환을 따름                     │
│  - interviewSlot은 INTERVIEW 상태에서만 허용   │
└─────────────┬──────────────────────────────┘
              │ has
              ▼
┌──────────────────────────────────────────┐
│      InterviewSlot (Value Object)        │
│------------------------------------------│
│ 필드:                                     │
│  - startTime                             │
│  - endTime                               │
│                                          │
│ 불변 조건:                                 │
│  - startTime < endTime                   │
│                                          │
│ 특성:                                     │
│  - 정체성 없음                              │
│  - 생명 주기 없음                           │
│  - 업데이트가 아닌 교체                       │
└──────────────────────────────────────────┘

이 다이어그램이 보여주는 건 InterviewSlot은 Application의 조건을 묘사하기 위해서만 존재한다는 거예요.

면접 시간이 바뀌면, 제품은 이전 슬롯을 기억할 필요가 없어요. 그냥 새 슬롯을 사용하면 되죠. “이 특정 면접 슬롯”이 변경을 통해 살아남는다는 개념이 없어요.

제품 관점에서 값 객체가 잘 작동하는 경우는 이래요.

만약 나중에 제품이 면접 슬롯을 팀이 예약하고, 일정을 변경하고, 독립적으로 추적하는 대상으로 추적하기 시작한다면, 그 변화는 경계 이동의 신호예요. 그 시점에서 그 개념은 정체성과 연속성이 필요해지고, 팀은 면접 슬롯을 엔티티로 여기기 시작해야 해요.


4. 값 객체를 별도로 모델링하는 이유

처음에는 시작 시간과 종료 시간을 지원서(Application)의 필드로 직접 저장하는 게 더 간단해 보일 수 있어요.

┌────────────────────────────────────────────┐
│            Application (엔티티)              │
│--------------------------------------------│
│ 식별자: applicationId                        │
│                                            │
│ 필드:                                       │
│  - candidateId                             │
│  - status                                  │
│  - interviewStartTime                      │
│  - interviewEndTime                        │
│                                            │
│ 규칙 / 불견조건:                              │
│  - 상태는 허용된 전환을 따름                    │
│  - interviewSlot은 INTERVIEW 상태에서만 허용   │
└────────────────────────────────────────────┘

위의 객체는 기술적으로는 작동하긴 해요. 하지만 이것은 중요한 질문을 흐려요.

단순히 타임스탬프를 저장하는 건가, 아니면 면접 슬롯을 모델링하는 건가?

제품이 “유효한 슬롯이 무엇인지”에 대해 신경 쓰기 시작하면, 별도의 값 객체가 유용해져요. InterviewSlot을 별도의 값 객체로 모델링하면 여러 목적을 달성할 수 있어요.

┌──────────────────────────────────────────┐
│             InterviewSlot (값 객체)        │
│------------------------------------------│
│ 필드:                                     │
│  - startTime                             │
│  - endTime                               │
│                                          │
│ 불변 조건:                                 │
│  - startTime < endTime                   │
│                                          │
│ 특성:                                     │
│  - 정체성 없음                              │
│  - 생명 주기 없음                           │
│  - 업데이트가 아닌 교체                       │
└──────────────────────────────────────────┘

네 가지 이유가 있어요.

1) 도메인 개념을 명시적으로 포착한다

도메인 전문가와 사용자는 “시작 시간과 종료 시간”이라고 말하지 않아요. “면접 슬롯”이라고 말하죠. 제품 논의에서 일관되게 등장하는 용어라면, 모델에도 존재할 자격이 있어요. 이것이 제품 공용 언어(Ubiquitous Language)의 기초예요.

2) 관련 제약을 맞는 개념에 상응시킨다

“시작 시간이 종료 시간보다 이전이어야 한다”는 규칙은 지원서(Application)에 대한 규칙이 아니에요. 시간 범위에 대한 규칙이죠. 면접 슬롯(InterviewSlot)에 넣으면 이런 제약이 다른 서비스나 엔티티에 흩어지는 것을 방지해요.

3) 재사용이 가능해진다

면접 슬롯(InterviewSlot)이 존재하면, 도메인의 다른 부분도 이것을 사용할 수 있어요. 면접관 가용성 확인, 일정 충돌 감지 같은 곳에서요. 이것이 없으면, 각 기능이 “유효한 시간 범위”의 자체 버전을 만들게 되고, 약간씩 다른 가정으로 구현되곤 해요.

4) 변경 비용이 낮아진다

시간 관련 규칙이 진화할 때, 변경할 곳이 하나라는 건 중요해요. 면접 시간 로직을 값 객체로 통합하면 변경의 파급 범위가 제한되죠.

좋은 판단 기준은 이런 질문을 하는 거예요.

이 중 하나라도 “예”라면, 처음에는 한 곳에서만 사용되더라도 값 객체가 적절할 수 있어요.


5. 값 객체가 엔티티로 바뀌는 순간: 양면 플랫폼 예시

제품이 양면 채용 플랫폼으로 진화한다고 상상해 보세요. 후보자와 면접관이 적극적으로 일정을 조율하는 플랫폼이에요.

이 새로운 모델에서 면접 슬롯은 더 이상 선택되고 저장되기만 하는 게 아니에요. 관리되는 거예요.

면접 슬롯이 이제 이런 일을 할 수 있어요.

플랫폼이 이렇게 작동하기 시작하면, 새로운 질문이 중요해져요.

이 질문들은 새로운 무언가를 도입해요. 연속성이에요.

제품이 시간에 걸쳐 같은 면접 슬롯을 다시 참조해야 하는 순간, 그 개념은 새로운 경계를 넘은 거예요. 더 이상 시간의 묘사가 아니에요. 제품이 시간에 걸쳐 같은 것으로 추적하는 무언가가 된 거죠.

그 시점에서 모델이 바뀌어요.

후기 단계 (양면 채용 플랫폼)
──────────────────────────────────────

┌──────────────────────────────────────────┐
│        InterviewSlot (엔티티)              │
│------------------------------------------│
│ 식별자: interviewSlotId                    │
│                                          │
│ 필드:                                     │
│  - startTime                             │
│  - endTime                               │
│                                          │
│ 규칙 / 불변조건:                            │
│  - 일정 변경은 허용된 규칙을 따름               │
│  - 취소는 기록됨                            │
│                                          │
│ 특성:                                     │
│  - 정체성 보존                              │
│  - 이력이 중요함                            │
│  - 변경이 추적됨                            │
└──────────────────────────────────────────┘

바뀐 건 제품에 대한 사용자의 기대예요.

내부 채용 관리 시스템에서 면접 시간은 “이 지원서의 면접은 언제인가?”에 답했어요. 양면 플랫폼에서 면접 슬롯은 “이 특정 일정 합의의 현재 상태는 무엇인가?”에 답하죠.

제품이 “이 특정 면접”이라고 말하고 과거에 대해 신경 쓰는 순간, 그 개념은 값 객체가 아니라 엔티티가 돼요.


6. 흔한 모델링 실수: 모든 것을 엔티티로, 변경 가능한 값 객체, 빈약한 엔티티

이 실수들은 특히 팀이 아직 도메인을 학습하는 중일 때 흔히 나타나요. 보통 합리적이었던 가정이 시간이 지나면서 합리적이지 않게 되면서 생기죠.

1) 모든 것을 엔티티로 만드는 실수

흔한 초기 결정은 모든 개념을 “만약을 위해” 엔티티로 모델링하는 거예요. 이러면 제품이 실제로 필요하지 않는 곳에 정체성과 생명 주기를 도입하게 돼요.

이것이 이끄는 결과는 불필요한 ID와 영속화 로직, 일관되게 유지해야 할 상태가 많아지는 것, 단순한 변경에 대한 조율 비용 상승이에요. 이렇게 되면 작은 기능 변경이 예상보다 오래 걸리고, 사용자는 신경 쓰지 않는데도 “정확히 어떤 것인가?”에 대한 논의가 계속되는 거예요.

2) 변경 가능한 값 객체

값 객체는 무언가를 묘사하기 위한 것이지, 시간에 걸쳐 추적하기 위한 게 아니에요. 값 객체가 내부적으로 변이되기 시작하면, 엔티티처럼 행동해요. 정체성이 암묵적이지만 명시적이지 않고, 이력이 존재하지만 모델링되지 않고, 경계가 불명확해지죠.

이건 그 개념이 실제로 정체성이 필요하거나, 변화를 소유해야 할 다른 빠진 개념이 있다는 거예요.

3) 빈약한 엔티티

빈약한 엔티티(Anemic Entity)는 데이터를 저장하지만 행동은 소유하지 않아요. 이런 모델에서는 엔티티가 필드만 갖고, 서비스가 모든 규칙을 강제하고, 행동이 묘사하는 개념에서 멀리 떨어져 살아요.

이 분리는 모델을 추론하기 어렵게 만들어요. 규칙이 흩어지고, 의도가 여러 계층에 걸쳐 희석되고, 제품을 이해하려면 여러 서비스를 읽어야 하죠.

모델링 실수를 비유하면, 회사의 직무 설계 실수와 같아요. 모든 직원에게 고유 사번(엔티티 ID)을 부여하는 건 좋지만, 인턴의 점심 주문에까지 고유 ID를 붙이고 추적하는 건 과잉이에요(모든 것을 엔티티로). 직무 기술서를 매주 바꾸면 누구의 직무인지 혼란스럽고(변경 가능한 값 객체), 직원에게 직함만 있고 권한은 전부 상사가 갖고 있으면 그 직원은 허수아비예요(빈약한 엔티티).


다음 편에서는 여러 개념에 걸친 비즈니스 규칙을 다루는 도메인 서비스(Domain Service)가 무엇이고, 다른 계층의 서비스와 어떻게 다른지를 살펴볼게요.

도메인 주도 설계 시리즈