애그리거트, 리포지터리 및 팩토리
애그리거트로 불변 조건을 보호하고 이를 깔끔하게 저장합니다.
애그리거트, 리포지터리 및 팩토리은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
불변 조건 보호
엔터티와 값 객체를 갖춘 다음에는 이들의 묶음을 일관되게 유지하고 깔끔하게 영속화하기 위한 패턴이 필요합니다. DDD는 세 가지 전술적 패턴으로 답합니다. 애그리거트(일관성 경계), 저장소(컬렉션과 같은 영속성 추상화), 팩터리(복잡한 생성)입니다. 이 레슨에서는 이들이 PHP에서 어떻게 함께 동작하는지 살펴봅니다.
애그리거트란 무엇인가
애그리거트는 데이터 변경을 위한 하나의 단위로 취급하는 엔터티와 값 객체의 묶음입니다. 그중 하나의 엔터티가 애그리거트 루트이며, 외부 코드가 참조를 보유할 수 있는 유일한 구성원입니다. 모든 변경은 루트를 거치고, 루트가 애그리거트의 불변 조건을 적용합니다. 애그리거트는 또한 트랜잭션 경계이므로 원자적으로 로드하고 저장합니다.
루트가 전체를 보호합니다
외부 호출자는 내부 구성원에 직접 접근하지 않습니다. 항목을 추가하려면 루트의 메서드를 호출하고, 루트가 유효성을 검사하며 일관성(총액, 한도)을 유지합니다. 이렇게 하면 불변 조건이 한곳에 모입니다.
<?php
final class LineItem {
public function __construct(
public readonly string $sku,
public readonly int $qty,
public readonly int $unitCents
) {}
public function subtotal(): int { return $this->qty * $this->unitCents; }
}
final class Order { // Aggregate Root
/** @var LineItem[] */
private array $items = [];
public function __construct(public readonly string $id) {}
public function addItem(string $sku, int $qty, int $unitCents): void {
if ($qty < 1) { throw new DomainException('qty must be >= 1'); }
$this->items[] = new LineItem($sku, $qty, $unitCents);
}
public function total(): int {
return array_sum(array_map(fn(LineItem $i) => $i->subtotal(), $this->items));
}
}
$o = new Order('o1');
$o->addItem('A', 2, 500);
$o->addItem('B', 1, 300);
echo $o->total(), PHP_EOL; // 1300
작은 애그리거트 설계
자주 하는 실수는 애그리거트를 지나치게 크게 만드는 것입니다(전체 Customer 그래프도 소유하는 Order 등). 일반적인 원칙은 다음과 같습니다.
- 애그리거트는 작게 유지하고, 다른 애그리거트는 객체를 보유하지 말고 식별자로 참조합니다.
- 하나의 트랜잭션에서는 하나의 애그리거트만 수정하고, 애그리거트 간 조정은 도메인 이벤트를 사용합니다.
- 항상 충족되어야 하는 불변 조건이 경계를 정의합니다.
식별자로 참조
주문은 Customer 엔터티가 아니라 customerId 값 객체를 저장합니다. 이렇게 하면 일관성 경계가 좁게 유지되고 거대한 객체 그래프를 로드하지 않아도 됩니다. 애그리거트 간 일관성은 하나의 거대한 트랜잭션이 아니라 이벤트로 처리되는 최종적 일관성이 됩니다.
<?php
final class CustomerId {
public function __construct(public readonly string $value) {}
}
final class Order {
public function __construct(
public readonly string $id,
public readonly CustomerId $customerId // reference, not object
) {}
}
$order = new Order('o1', new CustomerId('cus_99'));
echo $order->customerId->value, PHP_EOL;
저장소 계약
저장소는 애그리거트 루트의 메모리 내 컬렉션처럼 보이게 합니다. 도메인은 인터페이스에만 의존하고, 구현(Doctrine, PDO, 메모리 내 구현)은 인프라 계층에 둡니다. 저장소는 전체 애그리거트를 다루며 부분 행은 다루지 않습니다.
<?php
interface OrderRepository {
public function ofId(string $id): ?Order;
public function save(Order $order): void;
public function nextIdentity(): string;
}
메모리 내 구현
메모리 내 저장소는 데이터베이스 없이 빠르게 수행하는 단위 테스트에 매우 유용합니다. 도메인이 인터페이스에 의존하므로 구현을 자유롭게 바꿀 수 있습니다(의존성 역전이 실제로 적용된 예입니다).
<?php
interface OrderRepository {
public function ofId(string $id): ?object;
public function save(object $order): void;
public function nextIdentity(): string;
}
final class Order { public function __construct(public readonly string $id) {} }
final class InMemoryOrderRepository implements OrderRepository {
private array $store = [];
public function ofId(string $id): ?object { return $this->store[$id] ?? null; }
public function save(object $order): void { $this->store[$order->id] = $order; }
public function nextIdentity(): string { return 'o_' . bin2hex(random_bytes(4)); }
}
$repo = new InMemoryOrderRepository();
$repo->save(new Order('o1'));
var_dump($repo->ofId('o1') !== null);
저장소는 DAO가 아닙니다
저장소는 일반적인 생성·조회·수정·삭제 DAO가 아닙니다. 저장소는 도메인의 의미가 담긴 질의(findOverdueOrders())를 제공하고, 불변 조건이 유지된 전체 애그리거트를 재구성합니다. 저장소는 도메인이 영속성을 몰라도 되도록 질의문과 객체 관계 매핑의 세부 사항을 의도적으로 숨깁니다. 질의 작성기나 save($anyEntity) 같은 일반 메서드가 도메인으로 새어 나오지 않게 하십시오.
복잡한 생성을 위한 팩터리
애그리거트를 생성할 때 식별자 생성, 값 객체 조립, 생성 시점의 불변 조건 적용과 같은 실제 논리가 필요하다면 이를 팩터리(전용 클래스 또는 이름이 지정된 정적 생성자)로 옮기십시오. 이렇게 하면 엔터티의 생성자가 본래 역할에 충실해지고 유효한 생성 규칙을 한곳에서 관리할 수 있습니다.
<?php
final class Order {
private function __construct(
public readonly string $id,
public readonly string $customerId
) {}
public static function place(string $customerId): self {
if ($customerId === '') { throw new DomainException('customer required'); }
return new self('o_' . bin2hex(random_bytes(4)), $customerId);
}
}
$order = Order::place('cus_1');
echo $order->id, PHP_EOL;
이들이 협력하는 방식
애플리케이션 서비스에서 일반적인 흐름은 다음과 같습니다.
- 팩터리(또는 이름이 지정된 생성자)가 유효한 애그리거트를 생성합니다.
- 애그리거트 루트의 메서드가 사용하는 동안 불변 조건을 적용합니다.
- 저장소가 전체 애그리거트를 영속화하고 나중에 재구성합니다.
애플리케이션 서비스는 애그리거트마다 하나의 트랜잭션 안에서 이 작업들을 조정하며, 인터페이스에만 의존합니다.
전체 애그리거트 불변 조건 적용
루트의 진정한 가치는 여러 구성원에 걸친 불변 조건을 적용하는 데 있습니다. 여기서는 주문 총액이 신용 한도를 초과하게 만드는 항목을 주문이 거부합니다. 이는 하나의 LineItem만으로는 적용할 수 없는 규칙입니다. 모든 변경이 루트를 거치므로 이 규칙을 절대로 우회할 수 없습니다.
<?php
final class Order {
private array $items = [];
public function __construct(
public readonly string $id,
private int $creditLimitCents
) {}
public function addItem(int $cents): void {
if ($this->total() + $cents > $this->creditLimitCents) {
throw new DomainException('Exceeds credit limit');
}
$this->items[] = $cents;
}
public function total(): int { return array_sum($this->items); }
}
$o = new Order('o1', 1000);
$o->addItem(600);
try { $o->addItem(600); } catch (DomainException $e) { echo $e->getMessage(), PHP_EOL; }
echo $o->total(), PHP_EOL; // 600
빠른 확인
애그리거트 설계
복습
불변 조건을 보호하고 깔끔하게 영속화하는 방법을 배웠습니다. 애그리거트는 일관성과 트랜잭션의 경계를 형성하며, 루트를 통해서만 변경되고 다른 애그리거트는 식별자로 참조하여 작게 유지됩니다. 저장소는 도메인 인터페이스 뒤에서 애그리거트 루트를 컬렉션처럼 제공하고 객체 관계 매핑과 질의문을 숨기며 메모리 내 테스트 대역을 사용할 수 있게 합니다. 팩터리는 복잡하고 불변 조건을 적용하는 생성을 한곳에 모읍니다. 이들은 함께 도메인 모델의 일관성을 유지하고 영속성으로부터 독립시킵니다.
AI 튜터와 함께 PHP을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 49
- 레슨
- 195
자주 묻는 질문
“애그리거트, 리포지터리 및 팩토리” 강의는 무료인가요?
네 — “애그리거트, 리포지터리 및 팩토리” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“애그리거트, 리포지터리 및 팩토리”에서 뭘 배우나요?
애그리거트로 불변 조건을 보호하고 이를 깔끔하게 저장합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PHP Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“애그리거트, 리포지터리 및 팩토리” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- DDD 구성 요소: 엔터티와 값 객체
- 애그리거트, 리포지터리 및 팩토리
- 도메인 이벤트와 도메인 서비스
- 바운디드 컨텍스트와 컨텍스트 매핑