DDD 구성 요소: 엔터티와 값 객체
풍부한 엔터티와 변경 불가능한 값 객체로 도메인을 모델링합니다.
DDD 구성 요소: 엔터티와 값 객체은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
전술적 핵심
도메인 주도 설계의 전술적 패턴은 코드에서 비즈니스 개념을 모델링하기 위한 어휘를 제공합니다. 두 가지 기본 구성 요소는 엔터티(시간이 지나도 정체성으로 정의되는 객체)와 값 객체(속성으로 정의되고 불변으로 취급되는 객체)입니다. 이 구분을 올바르게 이해하는 것이 도메인 계층의 나머지 모든 것을 형성합니다.
식별자와 값의 차이
엔터티는 속성이 변경되더라도 지속되는 안정적인 식별성을 가집니다. 예를 들어 Customer는 이사한 후에도 같은 고객입니다. 값 객체에는 식별성이 없습니다. 속성이 같은 두 값 객체는 5달러 지폐 두 장처럼 서로 바꿔 사용할 수 있습니다. 다음과 같이 자문해 보십시오. “모든 필드가 바뀌어도 여전히 같은 것인가요?” 그렇다면 엔터티입니다.
값 객체
값 객체는 불변이며 자체 검증을 수행합니다. PHP 8.1의 readonly 속성은 불변성을 강제하고, 생성자는 불변 조건을 검사하여 유효하지 않은 인스턴스가 절대로 존재할 수 없도록 합니다.
<?php
final class Email {
public function __construct(public readonly string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}
public function equals(Email $other): bool {
return strtolower($this->value) === strtolower($other->value);
}
}
$a = new Email('Jane@Example.com');
var_dump($a->equals(new Email('jane@example.com'))); // true
값에 따른 동등성
값 객체는 참조가 아니라 구조적 동등성으로 비교합니다. PHP의 느슨한 비교에는 예상하기 어려운 규칙이 있으므로 ==에 의존하지 말고 명시적인 equals() 메서드를 제공하십시오. Money 값 객체는 금액과 통화를 모두 고려해야 합니다.
<?php
final class Money {
public function __construct(
public readonly int $cents,
public readonly string $currency
) {}
public function equals(Money $o): bool {
return $this->cents === $o->cents && $this->currency === $o->currency;
}
public function add(Money $o): self {
if ($this->currency !== $o->currency) {
throw new DomainException('Currency mismatch');
}
return new self($this->cents + $o->cents, $this->currency);
}
}
$sum = (new Money(500, 'USD'))->add(new Money(250, 'USD'));
echo $sum->cents, PHP_EOL; // 750
불변성과 'with' 메서드
값 객체는 불변이므로 하나를 “변경한다”는 것은 새 인스턴스를 반환한다는 뜻입니다. add()나 withAmount() 같은 메서드는 절대로 값을 변경하지 않고 새로운 값을 만듭니다. 따라서 별칭으로 인한 버그가 사라지고, 도메인 전체에서 값 객체를 자유롭게 공유할 수 있습니다.
<?php
final class DateRange {
public function __construct(
public readonly DateTimeImmutable $start,
public readonly DateTimeImmutable $end
) {
if ($end < $start) { throw new InvalidArgumentException('end before start'); }
}
public function withEnd(DateTimeImmutable $end): self {
return new self($this->start, $end);
}
}
$r = new DateRange(new DateTimeImmutable('2026-01-01'), new DateTimeImmutable('2026-01-10'));
$r2 = $r->withEnd(new DateTimeImmutable('2026-02-01'));
echo $r->end->format('Y-m-d'), ' / ', $r2->end->format('Y-m-d'), PHP_EOL;
엔터티
엔터티는 데이터베이스의 자동 증가 값이 아니라 일반적으로 도메인에서 생성한 식별자로 정의됩니다. 동등성은 식별자를 비교합니다. 속성은 불변 조건을 보호하는 풍부한 동작의 메서드를 통해 변경할 수 있습니다.
<?php
final class Customer {
private Email $email;
public function __construct(
public readonly string $id,
Email $email
) { $this->email = $email; }
public function changeEmail(Email $new): void { $this->email = $new; }
public function email(): Email { return $this->email; }
public function sameIdentityAs(Customer $o): bool { return $this->id === $o->id; }
}
$c = new Customer('cus_1', new Email('a@b.com'));
$c->changeEmail(new Email('c@d.com'));
echo $c->email()->value, PHP_EOL; // c@d.com
식별자 생성
데이터베이스에 저장되기를 기다리기보다 영속화하기 전에 도메인에서 식별자(예: UUID)를 생성하는 편이 좋습니다. 이렇게 하면 메모리에서 완전히 유효한 엔터티를 만들고, 여러 애그리거트에서 참조하며, 데이터베이스 없이 테스트할 수 있습니다. 식별자는 저장소의 부산물이 아니라 모델의 일부입니다.
풍부한 동작, 빈약한 데이터가 아님
흔한 안티패턴은 빈혈 도메인 모델입니다. 엔터티는 공개 조회자와 설정자의 모음일 뿐이고 모든 논리는 “서비스” 클래스에 들어 있는 형태입니다. DDD에서는 동작을 엔터티 안으로 넣습니다. $order->cancel()은 호출자가 상태 필드를 직접 바꾸도록 두지 않고 취소 규칙을 캡슐화합니다.
<?php
final class Order {
private string $status = 'open';
public function __construct(public readonly string $id) {}
public function cancel(): void {
if ($this->status === 'shipped') {
throw new DomainException('Cannot cancel a shipped order');
}
$this->status = 'cancelled';
}
public function status(): string { return $this->status; }
}
$o = new Order('o1');
$o->cancel();
echo $o->status(), PHP_EOL; // cancelled
어디서나 값 객체 사용
원시 타입 집착을 값 객체로 바꾸십시오. string $email, int $cents, string $currency를 전달하는 대신 Email과 Money로 감싸십시오. 이점은 다음과 같습니다.
- 생성할 때 한 번만 검증합니다.
- 도메인 규칙(통화 일치)이 데이터와 함께 존재합니다.
- 형식 시그니처가 의도를 설명하고 혼동을 방지합니다.
영속성 매핑 참고
값 객체는 자체 테이블이 아니라 내장 열로 매핑되는 경우가 많습니다(Doctrine의 #[Embeddable]). 엔터티는 식별자를 키로 하는 행에 매핑됩니다. 영속성에 관한 관심사를 도메인 객체 자체에서 분리하십시오. 모델은 자신이 MySQL에 저장된다는 사실을 알 필요가 없습니다. 객체 관계 매퍼나 직접 작성한 매퍼가 풍부한 도메인과 데이터베이스 사이를 변환합니다.
복합 값 객체
값 객체는 조합할 수 있습니다. Address는 여러 원시 타입을 자체적인 동등성과 형식 지정을 갖춘 하나의 응집된 개념으로 묶습니다. 그러면 엔터티는 서로 관련 없는 문자열 다섯 개 대신 하나의 풍부한 형식을 보유하고, 주소 규칙은 한곳에 모입니다.
<?php
final class Address {
public function __construct(
public readonly string $street,
public readonly string $city,
public readonly string $postcode
) {
if ($postcode === '') { throw new InvalidArgumentException('postcode required'); }
}
public function equals(Address $o): bool {
return $this->street === $o->street
&& $this->city === $o->city
&& $this->postcode === $o->postcode;
}
public function oneLine(): string {
return "{$this->street}, {$this->city} {$this->postcode}";
}
}
echo (new Address('1 Main St', 'Ankara', '06000'))->oneLine(), PHP_EOL;
빠른 확인
엔터티일까요, 값 객체일까요?
복습
DDD의 두 가지 핵심 구성 요소를 배웠습니다. 엔터티는 안정적인 식별성으로 정의되고, 풍부한 동작을 가지며, 메서드를 통해 불변 조건을 보호합니다. 값 객체는 불변이고 자체 검증을 수행하며, 속성의 동등성으로 비교되어 원시 타입 집착을 없앱니다. 빈약한 데이터 묶음이 아니라 객체 안에 논리를 넣어 이를 올바르게 모델링하는 것이 애그리거트, 저장소 및 나머지 도메인 계층의 토대입니다.
자주 묻는 질문
“DDD 구성 요소: 엔터티와 값 객체” 강의는 무료인가요?
네 — “DDD 구성 요소: 엔터티와 값 객체” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“DDD 구성 요소: 엔터티와 값 객체”에서 뭘 배우나요?
풍부한 엔터티와 변경 불가능한 값 객체로 도메인을 모델링합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PHP Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“DDD 구성 요소: 엔터티와 값 객체” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- DDD 구성 요소: 엔터티와 값 객체
- 애그리거트, 리포지터리 및 팩토리
- 도메인 이벤트와 도메인 서비스
- 바운디드 컨텍스트와 컨텍스트 매핑