Theory
중급 2-3시간 이론

DDD 이론 8: Repository, Factory, Module

영속성 추상화를 위한 Repository 패턴, 복잡한 객체 생성을 캡슐화하는 Factory 패턴, 그리고 코드 구조화를 위한 Module 설계 원칙을 심층적으로 학습합니다.

RepositoryFactoryModulePersistenceHexagonalPackage Design
학습 목표
  • • Repository 패턴의 본질과 컬렉션 인터페이스를 이해한다
  • • 다양한 Repository 구현 전략을 적용할 수 있다
  • • Factory 패턴으로 복잡한 객체 생성을 캡슐화할 수 있다
  • • Module 설계 원칙과 패키지 구조 패턴을 이해한다
  • • 레이어 간 의존성 규칙을 준수하는 구조를 설계할 수 있다

1. Repository 패턴의 본질

"Repository는 특정 타입의 모든 객체를 개념적으로 집합(Set)처럼 표현한다. 컬렉션처럼 동작하지만, 더 정교한 쿼리 기능을 제공한다."

— Eric Evans, Domain-Driven Design

1.1 Repository가 필요한 이유

❌ Repository 없이

class OrderService {
  async createOrder(dto: CreateOrderDto) {
    // 도메인 로직이 인프라에 의존
    const order = new Order();
    order.customerId = dto.customerId;
    
    // SQL 직접 사용 - 도메인 오염
    await db.query(
      'INSERT INTO orders ...',
      [order.id, order.customerId]
    );
    
    // ORM 직접 사용 - 영속성 누출
    await this.entityManager.save(order);
  }
}

도메인 레이어가 데이터베이스에 직접 의존

✅ Repository 사용

class OrderService {
  constructor(
    private orderRepository: OrderRepository
  ) {}

  async createOrder(dto: CreateOrderDto) {
    // 순수한 도메인 로직
    const order = Order.create(
      CustomerId.from(dto.customerId),
      Address.from(dto.address)
    );
    
    // 영속성 추상화
    await this.orderRepository.save(order);
  }
}

도메인 레이어는 영속성 메커니즘을 모름

1.2 Repository의 핵심 특성

1. 컬렉션 인터페이스

Repository는 인메모리 컬렉션처럼 느껴져야 합니다. add, remove, find 같은 컬렉션 메서드를 제공합니다.

2. Aggregate Root 전용

Repository는 Aggregate Root만 다룹니다. 내부 Entity나 Value Object는 Root와 함께 저장/로드됩니다.

3. 도메인 레이어에 인터페이스 정의

인터페이스는 도메인 레이어에, 구현은 인프라 레이어에 위치합니다. 의존성 역전 원칙(DIP)을 따릅니다.

4. 도메인 객체 반환

Repository는 항상 도메인 객체(Aggregate)를 반환합니다. DTO나 Entity가 아닌 완전한 도메인 모델을 반환합니다.

1.3 Repository 인터페이스 설계

기본 Repository 인터페이스
// 도메인 레이어에 정의
interface Repository<T extends AggregateRoot<ID>, ID> {
  findById(id: ID): Promise<T | null>;
  save(aggregate: T): Promise<void>;
  delete(aggregate: T): Promise<void>;
  exists(id: ID): Promise<boolean>;
}

// 구체적인 Repository 인터페이스
interface OrderRepository extends Repository<Order, OrderId> {
  // 기본 메서드는 상속
  
  // 도메인 특화 쿼리 메서드
  findByCustomerId(customerId: CustomerId): Promise<Order[]>;
  findByStatus(status: OrderStatus): Promise<Order[]>;
  findPendingOrdersOlderThan(date: Date): Promise<Order[]>;
  
  // 페이징
  findAll(page: PageRequest): Promise<Page<Order>>;
  
  // 카운트
  countByStatus(status: OrderStatus): Promise<number>;
}

// 사용 예시
class OrderApplicationService {
  constructor(private readonly orderRepository: OrderRepository) {}

  async getOrder(orderId: string): Promise<OrderDto> {
    const order = await this.orderRepository.findById(
      OrderId.from(orderId)
    );
    
    if (!order) {
      throw new OrderNotFoundError(orderId);
    }
    
    return OrderDto.from(order);
  }

  async confirmOrder(orderId: string): Promise<void> {
    const order = await this.orderRepository.findById(
      OrderId.from(orderId)
    );
    
    if (!order) {
      throw new OrderNotFoundError(orderId);
    }
    
    order.confirm();
    
    await this.orderRepository.save(order);
  }
}

1.4 Repository 구현 전략

전략장점단점적합한 경우
ORM 기반생산성, 익숙함임피던스 불일치일반적인 CRUD
순수 SQL성능 최적화매핑 코드 많음복잡한 쿼리
Document DBAggregate 자연스럽게조인 어려움복잡한 Aggregate
Event Sourcing완전한 이력복잡성감사 필수

2. Repository 구현 패턴

2.1 TypeORM 기반 구현

// 인프라 레이어: TypeORM Repository 구현
class TypeORMOrderRepository implements OrderRepository {
  constructor(
    private readonly dataSource: DataSource,
    private readonly mapper: OrderMapper
  ) {}

  async findById(id: OrderId): Promise<Order | null> {
    const entity = await this.dataSource
      .getRepository(OrderEntity)
      .findOne({
        where: { id: id.value },
        relations: ['lineItems', 'payments']
      });
    
    return entity ? this.mapper.toDomain(entity) : null;
  }

  async save(order: Order): Promise<void> {
    const entity = this.mapper.toEntity(order);
    
    await this.dataSource.transaction(async manager => {
      // Orphan removal: 기존 항목 삭제
      await manager.delete(OrderLineItemEntity, { orderId: order.id.value });
      
      // Aggregate 전체 저장
      await manager.save(OrderEntity, entity);
    });
  }

  async delete(order: Order): Promise<void> {
    await this.dataSource
      .getRepository(OrderEntity)
      .delete({ id: order.id.value });
  }

  async findByCustomerId(customerId: CustomerId): Promise<Order[]> {
    const entities = await this.dataSource
      .getRepository(OrderEntity)
      .find({
        where: { customerId: customerId.value },
        relations: ['lineItems'],
        order: { createdAt: 'DESC' }
      });
    
    return entities.map(e => this.mapper.toDomain(e));
  }

  async findByStatus(status: OrderStatus): Promise<Order[]> {
    const entities = await this.dataSource
      .getRepository(OrderEntity)
      .find({
        where: { status: status.value },
        relations: ['lineItems']
      });
    
    return entities.map(e => this.mapper.toDomain(e));
  }

  async exists(id: OrderId): Promise<boolean> {
    const count = await this.dataSource
      .getRepository(OrderEntity)
      .count({ where: { id: id.value } });
    return count > 0;
  }
}

2.2 Mapper 패턴

// 도메인 ↔ 영속성 변환
class OrderMapper {
  toDomain(entity: OrderEntity): Order {
    return Order.reconstitute({
      id: OrderId.from(entity.id),
      customerId: CustomerId.from(entity.customerId),
      status: OrderStatus.from(entity.status),
      items: entity.lineItems.map(item => 
        OrderLineItem.create(
          item.lineNumber,
          ProductId.from(item.productId),
          item.productName,
          Quantity.of(item.quantity),
          Money.of(item.unitPrice, Currency.from(item.currency))
        )
      ),
      shippingAddress: Address.create({
        street: entity.shippingStreet,
        city: entity.shippingCity,
        zipCode: entity.shippingZipCode
      }),
      totalAmount: Money.of(entity.totalAmount, Currency.KRW),
      createdAt: entity.createdAt,
      version: entity.version
    });
  }

  toEntity(order: Order): OrderEntity {
    const entity = new OrderEntity();
    entity.id = order.id.value;
    entity.customerId = order.customerId.value;
    entity.status = order.status.value;
    entity.totalAmount = order.totalAmount.amount;
    entity.shippingStreet = order.shippingAddress.street;
    entity.shippingCity = order.shippingAddress.city;
    entity.shippingZipCode = order.shippingAddress.zipCode;
    entity.createdAt = order.createdAt;
    entity.version = order.version;
    
    entity.lineItems = order.items.map(item => {
      const lineEntity = new OrderLineItemEntity();
      lineEntity.orderId = order.id.value;
      lineEntity.lineNumber = item.lineNumber;
      lineEntity.productId = item.productId.value;
      lineEntity.productName = item.productName;
      lineEntity.quantity = item.quantity.value;
      lineEntity.unitPrice = item.unitPrice.amount;
      lineEntity.currency = item.unitPrice.currency.code;
      return lineEntity;
    });
    
    return entity;
  }
}

2.3 In-Memory Repository (테스트용)

// 테스트용 In-Memory 구현
class InMemoryOrderRepository implements OrderRepository {
  private orders: Map<string, Order> = new Map();

  async findById(id: OrderId): Promise<Order | null> {
    return this.orders.get(id.value) ?? null;
  }

  async save(order: Order): Promise<void> {
    // 깊은 복사로 저장 (불변성 보장)
    this.orders.set(order.id.value, this.clone(order));
  }

  async delete(order: Order): Promise<void> {
    this.orders.delete(order.id.value);
  }

  async findByCustomerId(customerId: CustomerId): Promise<Order[]> {
    return Array.from(this.orders.values())
      .filter(order => order.customerId.equals(customerId));
  }

  async findByStatus(status: OrderStatus): Promise<Order[]> {
    return Array.from(this.orders.values())
      .filter(order => order.status === status);
  }

  async exists(id: OrderId): Promise<boolean> {
    return this.orders.has(id.value);
  }

  // 테스트 헬퍼
  clear(): void {
    this.orders.clear();
  }

  count(): number {
    return this.orders.size;
  }

  private clone(order: Order): Order {
    // 직렬화/역직렬화로 깊은 복사
    return Order.reconstitute(JSON.parse(JSON.stringify(order)));
  }
}

// 테스트에서 사용
describe('OrderService', () => {
  let orderRepository: InMemoryOrderRepository;
  let orderService: OrderService;

  beforeEach(() => {
    orderRepository = new InMemoryOrderRepository();
    orderService = new OrderService(orderRepository);
  });

  it('should create order', async () => {
    const orderId = await orderService.createOrder({
      customerId: 'cust-1',
      items: [{ productId: 'prod-1', quantity: 2 }]
    });

    const order = await orderRepository.findById(OrderId.from(orderId));
    expect(order).not.toBeNull();
    expect(order?.items).toHaveLength(1);
  });
});

2.4 Specification 패턴

// 복잡한 쿼리 조건을 캡슐화
interface Specification<T> {
  isSatisfiedBy(candidate: T): boolean;
  toQuery(): QueryCondition;  // DB 쿼리로 변환
}

class OrderByStatusSpec implements Specification<Order> {
  constructor(private readonly status: OrderStatus) {}

  isSatisfiedBy(order: Order): boolean {
    return order.status === this.status;
  }

  toQuery(): QueryCondition {
    return { status: this.status.value };
  }
}

class OrderByDateRangeSpec implements Specification<Order> {
  constructor(
    private readonly startDate: Date,
    private readonly endDate: Date
  ) {}

  isSatisfiedBy(order: Order): boolean {
    return order.createdAt >= this.startDate && 
           order.createdAt <= this.endDate;
  }

  toQuery(): QueryCondition {
    return {
      createdAt: Between(this.startDate, this.endDate)
    };
  }
}

// 조합 가능한 Specification
class AndSpecification<T> implements Specification<T> {
  constructor(
    private readonly left: Specification<T>,
    private readonly right: Specification<T>
  ) {}

  isSatisfiedBy(candidate: T): boolean {
    return this.left.isSatisfiedBy(candidate) && 
           this.right.isSatisfiedBy(candidate);
  }

  toQuery(): QueryCondition {
    return {
      ...this.left.toQuery(),
      ...this.right.toQuery()
    };
  }
}

// Repository에서 사용
interface OrderRepository {
  findBySpec(spec: Specification<Order>): Promise<Order[]>;
}

// 사용 예시
const pendingOrdersThisMonth = new AndSpecification(
  new OrderByStatusSpec(OrderStatus.PENDING),
  new OrderByDateRangeSpec(startOfMonth, endOfMonth)
);

const orders = await orderRepository.findBySpec(pendingOrdersThisMonth);

3. Factory 패턴

"복잡한 객체와 Aggregate의 인스턴스를 생성하는 책임을 별도의 객체에 위임하라. 이 객체 자체는 도메인 모델에서 책임이 없지만, 도메인 설계의 일부다."

— Eric Evans, Domain-Driven Design

3.1 Factory가 필요한 경우

1. 복잡한 Aggregate 생성

여러 Entity와 Value Object로 구성된 Aggregate를 생성할 때

2. 생성 로직이 도메인 지식을 포함

비즈니스 규칙에 따라 다른 객체를 생성해야 할 때

3. 외부 서비스 의존

생성 시 외부 서비스 호출이 필요할 때 (ID 생성, 검증 등)

4. 다형성 객체 생성

조건에 따라 다른 타입의 객체를 생성해야 할 때

3.2 Factory 구현 패턴

패턴 1: Aggregate Root의 팩토리 메서드
// 가장 간단한 형태: Aggregate Root에 팩토리 메서드
class Order {
  private constructor(id: OrderId) {
    // private 생성자
  }

  // 팩토리 메서드
  static create(
    customerId: CustomerId,
    shippingAddress: Address
  ): Order {
    const order = new Order(OrderId.generate());
    order._customerId = customerId;
    order._shippingAddress = shippingAddress;
    order._status = OrderStatus.DRAFT;
    order._items = [];
    order._createdAt = new Date();
    
    order.addDomainEvent(new OrderCreatedEvent(order.id));
    
    return order;
  }

  // 재구성용 팩토리 메서드 (Repository에서 사용)
  static reconstitute(props: OrderProps): Order {
    const order = new Order(props.id);
    order._customerId = props.customerId;
    order._shippingAddress = props.shippingAddress;
    order._status = props.status;
    order._items = props.items;
    order._createdAt = props.createdAt;
    order._version = props.version;
    return order;
  }
}
패턴 2: 별도 Factory 클래스
// 복잡한 생성 로직이 필요할 때
class OrderFactory {
  constructor(
    private readonly productRepository: ProductRepository,
    private readonly pricingService: PricingService,
    private readonly inventoryService: InventoryService
  ) {}

  async createOrder(
    customerId: CustomerId,
    items: CreateOrderItemDto[],
    shippingAddress: Address
  ): Promise<Order> {
    // 1. 상품 정보 조회
    const productIds = items.map(i => ProductId.from(i.productId));
    const products = await this.productRepository.findByIds(productIds);
    
    // 2. 재고 확인
    for (const item of items) {
      const available = await this.inventoryService.checkAvailability(
        ProductId.from(item.productId),
        Quantity.of(item.quantity)
      );
      if (!available) {
        throw new InsufficientInventoryError(item.productId);
      }
    }
    
    // 3. 가격 계산 (할인, 프로모션 적용)
    const pricedItems = await this.pricingService.calculatePrices(
      items,
      customerId
    );
    
    // 4. Order Aggregate 생성
    const order = Order.create(customerId, shippingAddress);
    
    for (const pricedItem of pricedItems) {
      const product = products.find(p => p.id.equals(pricedItem.productId));
      order.addItem(
        pricedItem.productId,
        product!.name,
        pricedItem.quantity,
        pricedItem.unitPrice
      );
    }
    
    return order;
  }
}

// Application Service에서 사용
class OrderApplicationService {
  constructor(
    private readonly orderFactory: OrderFactory,
    private readonly orderRepository: OrderRepository
  ) {}

  async createOrder(command: CreateOrderCommand): Promise<OrderId> {
    const order = await this.orderFactory.createOrder(
      CustomerId.from(command.customerId),
      command.items,
      Address.from(command.shippingAddress)
    );
    
    await this.orderRepository.save(order);
    
    return order.id;
  }
}
패턴 3: 다형성 Factory
// 조건에 따라 다른 타입 생성
abstract class Discount {
  abstract apply(amount: Money): Money;
}

class PercentageDiscount extends Discount {
  constructor(private readonly percentage: number) { super(); }
  
  apply(amount: Money): Money {
    return amount.multiply(this.percentage / 100);
  }
}

class FixedAmountDiscount extends Discount {
  constructor(private readonly fixedAmount: Money) { super(); }
  
  apply(amount: Money): Money {
    return this.fixedAmount.isGreaterThan(amount) 
      ? amount 
      : this.fixedAmount;
  }
}

class BuyOneGetOneDiscount extends Discount {
  apply(amount: Money): Money {
    return amount.multiply(0.5);
  }
}

// Factory
class DiscountFactory {
  create(coupon: Coupon): Discount {
    switch (coupon.type) {
      case CouponType.PERCENTAGE:
        return new PercentageDiscount(coupon.value);
      case CouponType.FIXED_AMOUNT:
        return new FixedAmountDiscount(Money.won(coupon.value));
      case CouponType.BOGO:
        return new BuyOneGetOneDiscount();
      default:
        throw new UnknownCouponTypeError(coupon.type);
    }
  }
}

3.3 Factory vs 생성자 선택 기준

상황권장
단순한 Value Object생성자 또는 정적 팩토리 메서드
단순한 EntityAggregate Root의 팩토리 메서드
복잡한 Aggregate별도 Factory 클래스
외부 서비스 의존별도 Factory 클래스
다형성 객체별도 Factory 클래스

4. Module(패키지) 설계

"Module은 관련된 개념들을 그룹화하고, 복잡성을 관리하는 방법이다. 좋은 Module은 높은 응집도와 낮은 결합도를 가진다."

— Eric Evans

4.1 Module 설계 원칙

1. 높은 응집도 (High Cohesion)

관련된 개념들을 함께 그룹화합니다. Module 내의 요소들은 서로 밀접하게 관련되어야 합니다.

2. 낮은 결합도 (Low Coupling)

Module 간의 의존성을 최소화합니다. 다른 Module의 내부 구현에 의존하지 않습니다.

3. 유비쿼터스 언어 반영

Module 이름은 도메인 언어를 반영해야 합니다. 기술적 용어가 아닌 비즈니스 용어를 사용합니다.

4. Bounded Context 정렬

Module 구조는 Bounded Context와 일치해야 합니다. 하나의 Context는 하나 이상의 Module로 구성됩니다.

4.2 패키지 구조 패턴

패턴 1: 레이어 기반 구조
src/
├── domain/                    # 도메인 레이어
│   ├── model/
│   │   ├── Order.ts
│   │   ├── OrderLineItem.ts
│   │   └── OrderStatus.ts
│   ├── repository/
│   │   └── OrderRepository.ts  # 인터페이스
│   ├── service/
│   │   └── OrderDomainService.ts
│   └── event/
│       └── OrderCreatedEvent.ts
│
├── application/               # 애플리케이션 레이어
│   ├── service/
│   │   └── OrderApplicationService.ts
│   ├── command/
│   │   └── CreateOrderCommand.ts
│   └── dto/
│       └── OrderDto.ts
│
├── infrastructure/            # 인프라 레이어
│   ├── persistence/
│   │   ├── TypeORMOrderRepository.ts
│   │   ├── OrderEntity.ts
│   │   └── OrderMapper.ts
│   └── messaging/
│       └── KafkaEventPublisher.ts
│
└── presentation/              # 프레젠테이션 레이어
    └── controller/
        └── OrderController.ts

장점: 레이어 간 의존성 명확, 익숙한 구조
단점: 기능 추가 시 여러 폴더 수정 필요

패턴 2: 기능(Feature) 기반 구조
src/
├── order/                     # 주문 기능
│   ├── domain/
│   │   ├── Order.ts
│   │   ├── OrderLineItem.ts
│   │   └── OrderRepository.ts
│   ├── application/
│   │   ├── CreateOrderUseCase.ts
│   │   └── ConfirmOrderUseCase.ts
│   ├── infrastructure/
│   │   └── TypeORMOrderRepository.ts
│   └── presentation/
│       └── OrderController.ts
│
├── customer/                  # 고객 기능
│   ├── domain/
│   ├── application/
│   ├── infrastructure/
│   └── presentation/
│
├── product/                   # 상품 기능
│   ├── domain/
│   ├── application/
│   ├── infrastructure/
│   └── presentation/
│
└── shared/                    # 공유 모듈
    ├── domain/
    │   ├── Money.ts
    │   └── Address.ts
    └── infrastructure/
        └── EventPublisher.ts

장점: 기능별 응집도 높음, 마이크로서비스 전환 용이
단점: 공유 코드 관리 필요

패턴 3: Hexagonal (Ports & Adapters)
src/
├── core/                      # 핵심 도메인 (헥사곤 내부)
│   ├── domain/
│   │   ├── model/
│   │   │   ├── Order.ts
│   │   │   └── OrderLineItem.ts
│   │   ├── port/
│   │   │   ├── in/            # Driving Ports (Use Cases)
│   │   │   │   ├── CreateOrderUseCase.ts
│   │   │   │   └── ConfirmOrderUseCase.ts
│   │   │   └── out/           # Driven Ports (Repositories, etc.)
│   │   │       ├── OrderRepository.ts
│   │   │       └── PaymentGateway.ts
│   │   └── service/
│   │       └── OrderDomainService.ts
│   └── application/
│       └── service/
│           └── OrderService.ts  # Use Case 구현
│
└── adapter/                   # 어댑터 (헥사곤 외부)
    ├── in/                    # Driving Adapters
    │   ├── web/
    │   │   └── OrderController.ts
    │   └── cli/
    │       └── OrderCli.ts
    └── out/                   # Driven Adapters
        ├── persistence/
        │   └── TypeORMOrderRepository.ts
        └── payment/
            └── StripePaymentGateway.ts

장점: 도메인 격리, 테스트 용이, 어댑터 교체 쉬움
단점: 초기 구조 복잡

4.3 Module 간 의존성 규칙

┌─────────────────────────────────────────────────────────────────┐
│                     의존성 방향 규칙                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────────┐                                           │
│   │  Presentation   │ ──────────────────────────────┐           │
│   │   (Controller)  │                               │           │
│   └────────┬────────┘                               │           │
│            │                                        │           │
│            ▼                                        │           │
│   ┌─────────────────┐                               │           │
│   │   Application   │ ──────────────────────────────┤           │
│   │   (Use Cases)   │                               │           │
│   └────────┬────────┘                               │           │
│            │                                        │           │
│            ▼                                        ▼           │
│   ┌─────────────────┐                    ┌─────────────────┐   │
│   │     Domain      │ ◄────────────────  │ Infrastructure  │   │
│   │ (Entities, VOs) │   의존성 역전      │  (Repositories) │   │
│   └─────────────────┘                    └─────────────────┘   │
│                                                                  │
│   규칙:                                                          │
│   • 상위 레이어 → 하위 레이어 의존 가능                         │
│   • Domain은 어떤 것에도 의존하지 않음                          │
│   • Infrastructure → Domain (인터페이스 구현)                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5. FAQ 및 핵심 요약

Q1: Repository와 DAO의 차이점은?

DAO는 데이터 접근 기술을 추상화하고, Repository는 도메인 객체의 컬렉션을 추상화합니다. Repository는 Aggregate 전체를 다루고 도메인 언어를 사용합니다.

Q2: Repository에 복잡한 쿼리를 넣어도 되나요?

도메인 관점의 쿼리는 괜찮습니다. 하지만 보고서용 복잡한 쿼리는 CQRS의 Query 모델로 분리하는 것이 좋습니다.

Q3: Factory는 항상 필요한가요?

아니요. 단순한 객체는 생성자나 정적 팩토리 메서드로 충분합니다. 복잡한 생성 로직이나 외부 의존성이 있을 때만 별도 Factory를 만드세요.

Q4: Module 구조는 어떻게 결정하나요?

팀 규모와 프로젝트 복잡도에 따라 결정합니다. 작은 프로젝트는 레이어 기반, 큰 프로젝트는 기능 기반이나 Hexagonal을 고려하세요.

Q5: In-Memory Repository는 언제 사용하나요?

단위 테스트에서 사용합니다. 실제 DB 없이 빠르게 테스트할 수 있고, 도메인 로직에 집중할 수 있습니다.

6. 핵심 요약

📦 Repository
  • • 컬렉션처럼 동작
  • • Aggregate Root 전용
  • • 인터페이스는 도메인에
  • • 구현은 인프라에
  • • 도메인 객체 반환
🏭 Factory
  • • 복잡한 생성 캡슐화
  • • 불변식 보장
  • • 다형성 객체 생성
  • • 외부 의존성 처리
  • • 단순하면 불필요
📁 Module
  • • 높은 응집도
  • • 낮은 결합도
  • • 도메인 언어 반영
  • • BC와 정렬
  • • 의존성 방향 준수
"Repository는 특정 타입의 모든 객체를 개념적으로 집합처럼 표현한다."

— Eric Evans

🎯 다음 학습

DDD Theory 09

Domain Events

DDD Theory 10

Application Service

DDD Theory 11

CQRS 패턴