0Pricing
PHP Academy · 강의

도메인 이벤트와 도메인 서비스

도메인 이벤트와 서비스로 비즈니스 사실을 포착합니다.

도메인 이벤트와 도메인 서비스은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

업무 사실 포착

모든 동작이 하나의 엔터티 안에 들어맞는 것은 아닙니다. 도메인 이벤트는 이미 발생한 의미 있는 업무 사실(OrderPlaced)을 포착하여 시스템의 다른 부분이 반응할 수 있게 합니다. 도메인 서비스는 특정 엔터티나 값 객체에 자연스럽게 속하지 않는 도메인 논리를 담습니다. 이 레슨에서는 애그리거트를 비대하게 만들지 않으면서 두 요소가 모델을 풍부하게 표현하는 방법을 살펴봅니다.

도메인 이벤트란 무엇인가

도메인 이벤트는 도메인에서 발생한 일을 기록한 불변 레코드입니다. 공통 언어를 사용해 과거형으로 이름을 붙입니다. 예를 들면 OrderPlaced, PaymentReceived, CustomerRelocated가 있습니다. 이벤트에는 그 사실을 설명하는 데이터와 발생 시각이 담깁니다. 값 객체와 마찬가지로 한 번 생성하면 변경하지 않습니다.

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
        public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
    ) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;

애그리거트가 이벤트를 기록합니다

애그리거트 루트는 동작의 일부로 이벤트를 기록한 다음, 트랜잭션이 커밋된 후에 애플리케이션 계층이 이를 전달할 수 있도록 공개합니다. 애그리거트 자체는 이벤트를 전달하지 않고 단지 기록만 합니다. 이렇게 하면 도메인에서 인프라 관련 관심사를 배제할 수 있습니다.

<?php
final class OrderPlaced {
    public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $out = $this->events; $this->events = []; return $out;
    }
}
final class Order extends AggregateRoot {
    public function __construct(public readonly string $id) {
        $this->record(new OrderPlaced($id));
    }
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }

커밋 후 전달

애플리케이션 서비스는 애그리거트를 영속화하고 커밋한 다음 공개된 이벤트를 전달합니다. 커밋 후에 전달하면 처리기가 롤백된 사실에 반응하는 일이 없습니다. 아래 순서는 이메일이나 외부 호출과 같은 부수 효과에 안전하게 사용할 수 있는 기본 구조입니다.

<?php
final class PlaceOrderService {
    public function __construct(
        private object $repo,
        private object $dispatcher
    ) {}
    public function handle(object $order): void {
        // 1. persist inside a transaction
        $this->repo->save($order);   // commit happens here
        // 2. only AFTER commit, release and dispatch
        foreach ($order->releaseEvents() as $event) {
            $this->dispatcher->dispatch($event);
        }
    }
}
echo "order persisted, then events dispatched", PHP_EOL;

처리기가 반응합니다

각 이벤트에는 이벤트를 발생시킨 애그리거트와 분리된 여러 처리기를 둘 수 있습니다. PSR-14 전달기가 이벤트를 등록된 수신기에게 전달합니다. 새로운 반응(분석, 알림)을 추가할 때는 애그리거트를 수정하지 않고 수신기만 추가하면 됩니다.

<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }

final class Dispatcher {
    private array $listeners = [];
    public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
    public function dispatch(object $event): void {
        foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
    }
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));

경계를 넘는 이벤트

도메인 이벤트는 트랜잭션을 공유하지 않고도 일관성을 유지해야 하는 애그리거트들을 분리합니다. 판매 컨텍스트의 OrderPlaced가 이벤트를 통해 다른 컨텍스트의 재고 예약을 촉발할 수 있습니다. 내부에서는 객체이지만 서비스 간에는 메시지 브로커의 통합 이벤트가 되는 경우가 많습니다. 내부 도메인 이벤트와 외부 통합 이벤트를 구분하십시오. 공개 계약이 내부 모델이 되어서는 안 됩니다.

도메인 서비스란 무엇인가

도메인 서비스는 다음과 같은 도메인 논리를 담습니다.

  • 여러 애그리거트 또는 엔터티가 관련되어 어느 하나에도 들어맞지 않고,
  • 공통 언어로 표현되는 진정한 도메인 개념입니다.

도메인 서비스는 상태가 없으며 도메인 객체를 대상으로 동작합니다. 한 계정에서 금액을 차감하고 다른 계정에 입금하는 FundsTransfer 서비스가 대표적인 예입니다.

코드로 보는 도메인 서비스

이 서비스는 두 애그리거트를 조정하지만 자체 상태는 보유하지 않습니다. 논리가 어느 계정에도 속하지 않으므로 도메인 작업의 이름을 딴 서비스에 둡니다.

<?php
final class Account {
    public function __construct(public readonly string $id, private int $cents) {}
    public function withdraw(int $c): void {
        if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
        $this->cents -= $c;
    }
    public function deposit(int $c): void { $this->cents += $c; }
    public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
    public function transfer(Account $from, Account $to, int $cents): void {
        $from->withdraw($cents);
        $to->deposit($cents);
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400

도메인 서비스와 애플리케이션 서비스

두 가지를 혼동하지 마십시오.

  • 도메인 서비스는 비즈니스 논리를 포함하며 도메인 계층에 있습니다. 트랜잭션이나 웹 요청에 대해서는 아무것도 알지 못합니다.
  • 애플리케이션 서비스는 사용 사례를 조정합니다. 저장소에서 애그리거트를 로드하고, 도메인 논리를 호출하고, 트랜잭션을 커밋한 다음 이벤트를 전달합니다. 자체적인 비즈니스 규칙이 없는 얇은 조정자입니다.

서비스 함정 피하기

도메인 서비스는 쉽게 과도하게 사용하게 됩니다. 논리를 엔터티나 값 객체에 둘 수 있다면 그렇게 해야 합니다. 이것이 빈혈 모델을 피하는 방법입니다. 작업이 실제로 여러 애그리거트에 걸치거나 자연스러운 위치가 전혀 없을 때만 도메인 서비스를 사용하십시오. 그렇지 않으면 엔터티에서 동작을 빼내고 절차적 코드를 다시 만들게 됩니다.

애플리케이션 서비스의 조정

전체를 연결해 보겠습니다. 얇은 애플리케이션 서비스는 저장소에서 집합체를 불러오고, 집합체 간 로직을 위해 도메인 서비스를 호출하며, 결과를 저장한 뒤 이벤트를 전달합니다. 이 서비스에는 조정 작업만 포함되며, 자체적인 비즈니스 규칙은 포함되지 않습니다.

<?php
final class Account {
    public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
    public function transfer(Account $from, Account $to, int $c): void {
        if ($c > $from->cents) { throw new DomainException('insufficient'); }
        $from->cents -= $c; $to->cents += $c;
    }
}
final class TransferApp { // application service: orchestration only
    public function __construct(private MoneyTransferService $domain) {}
    public function run(Account $a, Account $b, int $c): void {
        $this->domain->transfer($a, $b, $c); // then repo->save + dispatch
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250

간단히 확인하기

이벤트와 서비스입니다.

복습

비즈니스 사실과 주인을 잃은 로직을 포착했습니다. 도메인 이벤트는 변경할 수 없는 과거형 기록으로, 집합체가 기록하고 애플리케이션 계층이 커밋 후 전달합니다. 이를 통해 반응을 분리하고 집합체 경계를 넘을 수 있습니다. 도메인 서비스는 여러 집합체에 걸친 상태 없는 도메인 로직을 담으며, 단지 사용 사례를 조정하는 얇은 애플리케이션 서비스와는 다릅니다. 두 가지 모두 꼭 필요한 만큼만 사용하여, 동작이 속해야 할 곳인 엔터티와 값 객체에 남아 있도록 하십시오.

자주 묻는 질문

“도메인 이벤트와 도메인 서비스” 강의는 무료인가요?

네 — “도메인 이벤트와 도메인 서비스” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“도메인 이벤트와 도메인 서비스”에서 뭘 배우나요?

도메인 이벤트와 서비스로 비즈니스 사실을 포착합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

PHP Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“도메인 이벤트와 도메인 서비스” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. DDD 구성 요소: 엔터티와 값 객체
  2. 애그리거트, 리포지터리 및 팩토리
  3. 도메인 이벤트와 도메인 서비스
  4. 바운디드 컨텍스트와 컨텍스트 매핑
← PHP Academy(으)로 돌아가기