본문 바로가기
CS

도메인 주도 설계 정리 - ENTITY, VALUE OBJECT

by 준형코딩 2025. 6. 26.

 

 

도메인 주도 설계: 엔티티와 값 객체의 본질

복잡한 도메인을 위한 소프트웨어를 설계할 때, 모델을 구성하는 각 요소의 본질을 깊이 이해해야 한다. 도메인 주도 설계(DDD)는 이 과정을 돕는 두 가지 강력한 빌딩 블록, 엔티티(Entity)와 값 객체(Value Object)를 제시한다.


1. 엔티티 (Entity): 식별성으로 정의되는 연속적인 삶

엔티티의 근본적인 개념은 객체의 생명주기 내내 이어지는 추상적인 연속성이며, 그러한 추상적인 연속성은 여러 형태를 거쳐 전달된다는 것이다.

어떤 객체를 그 속성이 아닌, 고유한 식별성(Identity)으로 일차적으로 정의한다면, 우리는 그것을 엔티티라고 부른다. 엔티티의 핵심은 시간이 흘러도 변치 않는 정체성이다.

예를 들어 '고객'이라는 엔티티를 생각해보자. 한 고객의 주소, 연락처, 심지어 이름까지 바뀔 수 있다. 즉, 엔티티는 자신의 생명주기 동안 그 형태와 내용이 급격하게 바뀔 수 있다. 하지만 이 모든 변화에도 불구하고, 고객에게 부여된 고유한 '고객 ID'가 변하지 않는 한 우리는 그를 동일한 고객으로 인식하고 추적할 수 있다. 바로 이 끊어지지 않는 정체성의 흐름이 엔티티가 가진 '추상적인 연속성'의 본질이다.

엔티티 모델링: 정체성에 집중하라

엔티티의 클래스 정의와 책임, 속성, 연관관계는 엔티티에 포함된 특정 속성보다는 정체성에 초점을 맞춰야 한다.

따라서 엔티티를 모델링할 때는 단순히 데이터를 담는 컨테이너로 접근해서는 안 된다. 해당 객체의 속성 목록을 나열하기 전에, "이 객체를 어떻게 유일하게 식별하고, 그 생명주기를 어떻게 관리할 것인가?"를 먼저 고민해야 한다.

  • 책임과 기능: 엔티티의 기능(메서드)은 해당 정체성이 겪는 상태 변화와 비즈니스 행위를 중심으로 설계된다. (예: Order.cancel(), Customer.updateAddress())
  • 속성: 속성들은 특정 시점의 상태를 나타낼 뿐, 엔티티 그 자체를 정의하지 않는다.

설령 엔티티의 생명주기가 복잡하지 않더라도, 그 의미에 따라 식별성이 중요한 객체를 엔티티로 명확히 분류한다면, 도메인 모델은 한층 더 투명해지고 구현은 견고해진다.

식별 연산의 설계

엔티티를 효과적으로 다루려면, 이 식별성을 어떻게 생성하고 조회할지 결정해야 한다. 식별자는 애플리케이션이 직접 생성(예: UUID)할 수도, 외부 시스템이나 데이터베이스를 통해 부여받을 수도 있다. 중요한 것은 이 식별 연산이 도메인 모델의 일관성을 해치지 않도록 신중하게 설계되어야 한다는 점이다.


2. 값 객체 (Value Object): 속성이 곧 본질인 불변의 가치

모델에 포함된 어떤 요소의 속성에만 관심이 있다면 그것을 Value Object로 분류하라. ... value object는 불변적으로 다뤄라. ... 아무런 식별성도 부여하지 말고 entity를 유지하는데 필요한 설계상의 복잡성을 피하라.

엔티티와 달리, 어떤 객체는 '누구인가'가 전혀 중요하지 않고 오직 '무엇인가'만이 중요할 때가 있다. 이처럼 자신을 설명하는 속성들의 조합만으로 모든 것이 정의되는 객체를 우리는 값 객체(Value Object)라고 한다.

'돈'을 예로 들 수 있다. '5,000원'이라는 가치는 '5000'이라는 숫자와 '원'이라는 통화 단위의 조합으로 완벽하게 설명된다. 이 돈의 고유한 일련번호(식별성)에는 관심이 없다. 단지 그 가치에만 관심이 있을 뿐이다.

값 객체의 설계: 불변성과 풍부한 행위

값 객체를 설계할 때 두 가지 원칙이 핵심적이다.

  1. 불변성(Immutability): 값 객체는 한번 생성되면 절대 변하지 않는다. 만약 '5,000원'을 '7,000원'으로 바꿔야 한다면, 기존 객체를 수정하는 것이 아니라 new Money(7000, "KRW")처럼 완전히 새로운 객체를 생성하여 교체해야 한다. 이는 객체의 상태가 예측 불가능하게 변하는 부작용(Side Effect)을 원천적으로 차단하여 시스템을 매우 안정적으로 만든다.
  2. 의미 있는 기능 부여: 값 객체는 단순히 값을 담는 것을 넘어, 그 값과 관련된 행위를 스스로 가질 수 있다. 예를 들어 Money 객체는 add(otherMoney)와 같은 연산 기능을, DateRange 객체는 isOverlapped(otherRange)와 같은 기간 비교 기능을 가질 수 있다.

이처럼 값 객체에는 식별성을 부여하지 않음으로써, 엔티티를 유지하는 데 필요한 복잡한 생명주기 관리나 추적 메커니즘을 피할 수 있다.

값 객체를 포함한 연관관계 설계

값 객체는 보통 독립적으로 존재하기보다, 엔티티의 속성을 풍부하게 설명하는 역할을 한다. 주문(Order) 엔티티가 배송지 주소(ShippingAddress) 값 객체를 소유하는 것과 같다. 이 관계에서 배송지 주소는 주문의 일부이며, 그 자체로 독립적인 생명주기를 갖지 않는다.

결론적으로, 엔티티와 값 객체를 명확히 구분하고 올바르게 모델링하는 것은 복잡한 도메인을 명료하고 견고한 소프트웨어로 구현하는 핵심적인 첫걸음이다.