Sam Baek, The Dev's Corner

πŸ—οΈ DDD(Domain-Driven Design) μ™„λ²½ κ°€μ΄λ“œ (Entity, Aggregate, Repository)

11 Nov 2025

DDDλž€ 무엇인가


ν”„λ‘œκ·Έλž¨μ„ λ§Œλ“€ λ•Œ κ°€μž₯ μ€‘μš”ν•œ 것은?
λ°”λ‘œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ΄λ‹€.

ν•˜μ§€λ§Œ μ½”λ“œκ°€ λ³΅μž‘ν•΄μ§€λ©΄,
λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ—¬κΈ°μ €κΈ° 흩어지고,
μ–΄λ””κ°€ 핡심인지 μ•ŒκΈ° μ–΄λ €μ›Œμ§„λ‹€.

DDD(Domain-Driven Design)λŠ”
λΉ„μ¦ˆλ‹ˆμŠ€ 도메인을 μ€‘μ‹¬μœΌλ‘œ μ„€κ³„ν•˜λŠ” 방법이닀.

마치 레고 블둝을 μ‘°λ¦½ν•˜λ“―,
도메인을 의미 μžˆλŠ” λ‹¨μœ„λ‘œ λ‚˜λˆ„κ³ ,
각 λΈ”λ‘μ˜ 역할을 λͺ…ν™•νžˆ μ •μ˜ν•œλ‹€.

μ™œ DDDλ₯Ό λ°°μ›Œμ•Ό ν• κΉŒ?


이유 1: λ³΅μž‘λ„ 관리
큰 ν”„λ‘œμ νŠΈλ₯Ό 의미 μžˆλŠ” λ‹¨μœ„λ‘œ λΆ„ν•΄

이유 2: 도메인 전문가와 μ†Œν†΅
κ°œλ°œμžμ™€ λΉ„μ¦ˆλ‹ˆμŠ€ λ‹΄λ‹Ήμžκ°€ 같은 μ–Έμ–΄ μ‚¬μš©

이유 3: μœ μ§€λ³΄μˆ˜μ„± ν–₯상
λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λͺ…ν™•ν•˜κ²Œ 뢄리됨

이유 4: λ©΄μ ‘ ν•„μˆ˜
Entity, Aggregate, RepositoryλŠ” λ©΄μ ‘ 단골

κΈ°λ³Έ κ°œλ… μš”μ•½


🏷️ DDD 핡심 ꡬ성 μš”μ†Œ


1. Entity (μ—”ν‹°ν‹°)


κ°œλ…: κ³ μœ ν•œ μ‹λ³„μžλ₯Ό κ°€μ§„ 객체

νŠΉμ§•:

  • ID둜 ꡬ뢄 κ°€λŠ₯
  • 생λͺ…μ£ΌκΈ°κ°€ 있음
  • 속성이 λ°”λ€Œμ–΄λ„ λ™μΌν•œ 객체


학생 λΉ„μœ :
ν•™λ²ˆ(ID)으둜 κ΅¬λΆ„ν•˜λŠ” 학생
이름이 λ°”λ€Œμ–΄λ„ 같은 학생

@Entity
public class User {
    @Id
    private Long id; // μ‹λ³„μž
    private String name;
    private String email;
    
    // 같은 IDλ©΄ 같은 User
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }
}


2. Value Object (κ°’ 객체)


κ°œλ…: μ‹λ³„μž 없이 κ°’μœΌλ‘œλ§Œ κ΅¬λΆ„λ˜λŠ” 객체

νŠΉμ§•:

  • IDκ°€ μ—†μŒ
  • λΆˆλ³€(Immutable)
  • 값이 κ°™μœΌλ©΄ 같은 객체


돈 λΉ„μœ :
1λ§Œμ›μ§œλ¦¬ μ§€νλŠ” IDκ°€ μ—†μŒ
1λ§Œμ›μ΄λ©΄ λͺ¨λ‘ 같은 κ°€μΉ˜

public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    // λΆˆλ³€: μƒˆ 객체 λ°˜ν™˜
    public Money add(Money other) {
        return new Money(
            this.amount.add(other.amount),
            this.currency
        );
    }
    
    @Override
    public boolean equals(Object o) {
        Money money = (Money) o;
        return amount.equals(money.amount) && 
               currency.equals(money.currency);
    }
}


3. Aggregate (μ• κ·Έλ¦¬κ±°νŠΈ)


κ°œλ…: κ΄€λ ¨λœ κ°μ²΄λ“€μ˜ 묢음

νŠΉμ§•:

  • ν•˜λ‚˜μ˜ Root Entityκ°€ 쑴재
  • μ™ΈλΆ€μ—μ„œλŠ” Rootλ₯Ό ν†΅ν•΄μ„œλ§Œ μ ‘κ·Ό
  • νŠΈλžœμž­μ…˜ λ‹¨μœ„


κ°€μ‘± λΉ„μœ :
κ°€μž₯(Root)을 톡해 κ°€μ‘± κ΅¬μ„±μ›μ—κ²Œ μ ‘κ·Ό
μ™ΈλΆ€μ—μ„œ 직접 μžλ…€μ—κ²Œ 연락 μ•ˆ 함

// Orderκ°€ Aggregate Root
public class Order {
    @Id
    private Long id;
    
    private List<OrderItem> items = new ArrayList<>();
    private OrderStatus status;
    
    // Rootλ₯Ό ν†΅ν•΄μ„œλ§Œ μ•„μ΄ν…œ μΆ”κ°€
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
    }
    
    // μ™ΈλΆ€μ—μ„œ 직접 items μˆ˜μ • λΆˆκ°€
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }
}

class OrderItem {
    private Product product;
    private int quantity;
    // OrderItem은 λ…λ¦½μ μœΌλ‘œ 쑴재 λΆˆκ°€
}


4. Repository (리포지토리)


κ°œλ…: Aggregateλ₯Ό μ €μž₯/μ‘°νšŒν•˜λŠ” μΈν„°νŽ˜μ΄μŠ€

νŠΉμ§•:

  • 도메인 계측에 속함
  • μ»¬λ ‰μ…˜μ²˜λŸΌ λ™μž‘
  • Root만 Repositoryλ₯Ό 가짐


λ„μ„œκ΄€ λΉ„μœ :
λ„μ„œκ΄€(Repository)μ—μ„œ μ±…(Aggregate) λŒ€μΆœ/λ°˜λ‚©

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(Long id);
    List<Order> findByUserId(Long userId);
    void delete(Order order);
}


5. Domain Service (도메인 μ„œλΉ„μŠ€)


κ°œλ…: νŠΉμ • Entity에 μ†ν•˜μ§€ μ•ŠλŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직

@Service
public class TransferService {
    
    public void transfer(Account from, Account to, Money amount) {
        // μ—¬λŸ¬ Aggregate κ°„ 둜직
        from.withdraw(amount);
        to.deposit(amount);
    }
}


🏷️ 계측 ꡬ쑰


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Presentation Layer        β”‚  ← Controller, View
β”‚   (μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Application Layer         β”‚  ← Use Case, Service
β”‚   (μ‘μš© 계측)                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Domain Layer              β”‚  ← Entity, VO, Aggregate
β”‚   (도메인 계측 - 핡심!)     β”‚     Domain Service
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Infrastructure Layer      β”‚  ← Repository κ΅¬ν˜„
β”‚   (인프라 계측)              β”‚     DB, μ™ΈλΆ€ API
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


🏷️ Bounded Context (경계 μ»¨ν…μŠ€νŠΈ)


κ°œλ…: 도메인 λͺ¨λΈμ΄ μ μš©λ˜λŠ” 경계

μ‡Όν•‘λͺ° μ˜ˆμ‹œ:

[μ£Όλ¬Έ Context]
- Order (μ£Όλ¬Έ)
- OrderItem
- Customer (ꡬ맀자)

[배솑 Context]
- Delivery (배솑)
- Customer (수령자)

[결제 Context]
- Payment (결제)
- Customer (결제자)


같은 Customerμ§€λ§Œ Contextλ§ˆλ‹€ λ‹€λ₯Έ 의미!

μ‹€μ „ μ˜ˆμ‹œ


🏷️ μ£Όλ¬Έ μ‹œμŠ€ν…œ DDD 섀계


1. Value Object


// Money - λΆˆλ³€ κ°’ 객체
public class Money {
    private final BigDecimal amount;
    
    public Money(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("κΈˆμ•‘μ€ 0 이상");
        }
        this.amount = amount;
    }
    
    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }
    
    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(new BigDecimal(quantity)));
    }
}

// Address - λΆˆλ³€ κ°’ 객체
public class Address {
    private final String city;
    private final String street;
    private final String zipCode;
    
    public Address(String city, String street, String zipCode) {
        this.city = city;
        this.street = street;
        this.zipCode = zipCode;
    }
    
    // getter만 쑴재 (λΆˆλ³€)
}


2. Entity


@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @Embedded
    private Money price;
    
    private int stock;
    
    // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
    public void decreaseStock(int quantity) {
        if (this.stock < quantity) {
            throw new OutOfStockException("재고 λΆ€μ‘±");
        }
        this.stock -= quantity;
    }
}


3. Aggregate Root


@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    @ManyToOne
    private Customer customer;
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @Embedded
    private Address shippingAddress;
    
    private LocalDateTime orderedAt;
    
    // μƒμ„±μž - νŒ©ν† λ¦¬ λ©”μ„œλ“œ
    public static Order create(Customer customer, Address address) {
        Order order = new Order();
        order.customer = customer;
        order.shippingAddress = address;
        order.status = OrderStatus.PENDING;
        order.orderedAt = LocalDateTime.now();
        return order;
    }
    
    // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직: Rootλ₯Ό ν†΅ν•΄μ„œλ§Œ μ•„μ΄ν…œ μΆ”κ°€
    public void addItem(Product product, int quantity) {
        // 재고 확인
        product.decreaseStock(quantity);
        
        // μ•„μ΄ν…œ μΆ”κ°€
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
    }
    
    // 총 κΈˆμ•‘ 계산
    public Money calculateTotalPrice() {
        return items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
    
    // μ£Όλ¬Έ ν™•μ •
    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("μ£Όλ¬Έ ν•­λͺ©μ΄ μ—†μŒ");
        }
        this.status = OrderStatus.CONFIRMED;
    }
    
    // μ£Όλ¬Έ μ·¨μ†Œ
    public void cancel() {
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("λ°°μ†‘λœ 주문은 μ·¨μ†Œ λΆˆκ°€");
        }
        
        // 재고 볡ꡬ
        items.forEach(item -> 
            item.getProduct().increaseStock(item.getQuantity())
        );
        
        this.status = OrderStatus.CANCELLED;
    }
}

@Entity
class OrderItem {
    @Id
    @GeneratedValue
    private Long id;
    
    @ManyToOne
    private Product product;
    
    private int quantity;
    
    @Embedded
    private Money price; // μ£Όλ¬Έ λ‹Ήμ‹œ 가격
    
    public OrderItem(Product product, int quantity) {
        this.product = product;
        this.quantity = quantity;
        this.price = product.getPrice();
    }
    
    public Money getSubtotal() {
        return price.multiply(quantity);
    }
}


4. Repository


public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(Long id);
    List<Order> findByCustomerId(Long customerId);
    List<Order> findByStatus(OrderStatus status);
}

@Repository
public class JpaOrderRepository implements OrderRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    @Override
    public Order save(Order order) {
        if (order.getId() == null) {
            em.persist(order);
            return order;
        } else {
            return em.merge(order);
        }
    }
    
    @Override
    public Optional<Order> findById(Long id) {
        return Optional.ofNullable(em.find(Order.class, id));
    }
}


5. Domain Service


@Service
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    @Transactional
    public Long createOrder(CreateOrderCommand command) {
        // 1. Customer 쑰회
        Customer customer = customerRepository.findById(command.getCustomerId())
            .orElseThrow(() -> new IllegalArgumentException("고객 μ—†μŒ"));
        
        // 2. Order 생성 (Aggregate Root)
        Order order = Order.create(customer, command.getShippingAddress());
        
        // 3. μƒν’ˆ μΆ”κ°€
        for (OrderItemRequest item : command.getItems()) {
            Product product = productRepository.findById(item.getProductId())
                .orElseThrow(() -> new IllegalArgumentException("μƒν’ˆ μ—†μŒ"));
            
            order.addItem(product, item.getQuantity());
        }
        
        // 4. μ£Όλ¬Έ ν™•μ •
        order.confirm();
        
        // 5. μ €μž₯
        Order savedOrder = orderRepository.save(order);
        
        return savedOrder.getId();
    }
    
    @Transactional
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new IllegalArgumentException("μ£Όλ¬Έ μ—†μŒ"));
        
        // λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ€ Aggregate에 μœ„μž„
        order.cancel();
        
        orderRepository.save(order);
    }
}


6. Application Service (Use Case)


@Service
@RequiredArgsConstructor
public class OrderApplicationService {
    
    private final OrderService orderService;
    private final PaymentService paymentService;
    private final NotificationService notificationService;
    
    @Transactional
    public OrderResponse placeOrder(CreateOrderCommand command) {
        // 1. μ£Όλ¬Έ 생성
        Long orderId = orderService.createOrder(command);
        
        // 2. 결제 처리
        paymentService.processPayment(orderId, command.getPaymentMethod());
        
        // 3. μ•Œλ¦Ό 전솑
        notificationService.sendOrderConfirmation(orderId);
        
        // 4. 응닡 생성
        return OrderResponse.from(orderId);
    }
}


🏷️ νŒ¨ν‚€μ§€ ꡬ쑰


src/main/java/com/example/shop/
β”œβ”€β”€ domain/
β”‚   β”œβ”€β”€ order/
β”‚   β”‚   β”œβ”€β”€ Order.java              (Aggregate Root)
β”‚   β”‚   β”œβ”€β”€ OrderItem.java          (Entity)
β”‚   β”‚   β”œβ”€β”€ OrderStatus.java        (Enum)
β”‚   β”‚   β”œβ”€β”€ OrderRepository.java    (Interface)
β”‚   β”‚   └── OrderService.java       (Domain Service)
β”‚   β”œβ”€β”€ product/
β”‚   β”‚   β”œβ”€β”€ Product.java
β”‚   β”‚   └── ProductRepository.java
β”‚   └── common/
β”‚       β”œβ”€β”€ Money.java              (Value Object)
β”‚       └── Address.java            (Value Object)
β”œβ”€β”€ application/
β”‚   └── OrderApplicationService.java
β”œβ”€β”€ infrastructure/
β”‚   β”œβ”€β”€ persistence/
β”‚   β”‚   β”œβ”€β”€ JpaOrderRepository.java
β”‚   β”‚   └── JpaProductRepository.java
β”‚   └── messaging/
β”‚       └── KafkaEventPublisher.java
└── presentation/
    └── OrderController.java


μ‹€μ „ 체크리슀트


βœ… 도메인 λͺ¨λΈ 섀계


  • Aggregate 경계 λͺ…ν™•νžˆ μ •μ˜
  • Value Object 적극 ν™œμš©
  • EntityλŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 포함
  • λΆˆλ³€μ„± μ΅œλŒ€ν•œ μœ μ§€


βœ… Repository νŒ¨ν„΄


  • Aggregate Root만 Repository
  • 도메인 계측에 μΈν„°νŽ˜μ΄μŠ€
  • 인프라 계측에 κ΅¬ν˜„μ²΄
  • μ»¬λ ‰μ…˜μ²˜λŸΌ μ‚¬μš©


βœ… λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 μœ„μΉ˜


  • Entity/Aggregate에 μ΅œλŒ€ν•œ
  • Domain ServiceλŠ” μ΅œμ†Œν™”
  • Application ServiceλŠ” μ‘°μ •λ§Œ
  • ControllerλŠ” λ³€ν™˜λ§Œ


βœ… Bounded Context


  • Context 경계 λͺ…ν™•νžˆ
  • 같은 μš©μ–΄λ„ Contextλ§ˆλ‹€ 닀름
  • Context Map μž‘μ„±
  • λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ 경계


μš”μ•½


DDDλŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 도메인을 μ€‘μ‹¬μœΌλ‘œ μ„€κ³„ν•˜λŠ” 방법이닀.

πŸ’Ž 핡심 포인트:

  1. Entity: μ‹λ³„μž μžˆλŠ” 객체
  2. Value Object: λΆˆλ³€ κ°’ 객체
  3. Aggregate: κ΄€λ ¨ 객체 묢음
  4. Repository: μ €μž₯/쑰회 μΈν„°νŽ˜μ΄μŠ€
  5. Domain Service: Entity λ°– 둜직
  6. Bounded Context: λͺ¨λΈ 경계


πŸ“Œ 섀계 원칙:

원칙 μ„€λͺ…
도메인 μš°μ„  κΈ°μˆ λ³΄λ‹€ λΉ„μ¦ˆλ‹ˆμŠ€ μš°μ„ 
Ubiquitous Language 개발자-도메인 μ „λ¬Έκ°€ 동일 μš©μ–΄
Aggregate 경계 νŠΈλžœμž­μ…˜ 일관성 λ‹¨μœ„
λΆˆλ³€μ„± Value ObjectλŠ” λΆˆλ³€


πŸš€ Best Practices:

  • AggregateλŠ” μž‘κ²Œ μœ μ§€
  • RepositoryλŠ” Root만
  • λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ€ Entity에
  • Anemic Model μ§€μ–‘
  • κ³Όλ„ν•œ 좔상화 μ§€μ–‘
  • νŒ€κ³Ό 지속적 μ†Œν†΅


DDDλŠ” λ³΅μž‘ν•œ 도메인을 λ‹€λ£° λ•Œ 빛을 λ°œν•œλ‹€.
μž‘μ€ ν”„λ‘œμ νŠΈμ—λŠ” κ³Όν•  수 μžˆμ§€λ§Œ,
큰 ν”„λ‘œμ νŠΈμ—μ„œλŠ” μœ μ§€λ³΄μˆ˜μ„±μ„ 크게 ν–₯μƒμ‹œν‚¨λ‹€.