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λ λΉμ¦λμ€ λλ©μΈμ μ€μ¬μΌλ‘ μ€κ³νλ λ°©λ²μ΄λ€.
π ν΅μ¬ ν¬μΈνΈ:
- Entity: μλ³μ μλ κ°μ²΄
- Value Object: λΆλ³ κ° κ°μ²΄
- Aggregate: κ΄λ ¨ κ°μ²΄ λ¬Άμ
- Repository: μ μ₯/μ‘°ν μΈν°νμ΄μ€
- Domain Service: Entity λ° λ‘μ§
- Bounded Context: λͺ¨λΈ κ²½κ³
π μ€κ³ μμΉ:
| μμΉ | μ€λͺ |
|---|---|
| λλ©μΈ μ°μ | κΈ°μ λ³΄λ€ λΉμ¦λμ€ μ°μ |
| Ubiquitous Language | κ°λ°μ-λλ©μΈ μ λ¬Έκ° λμΌ μ©μ΄ |
| Aggregate κ²½κ³ | νΈλμμ μΌκ΄μ± λ¨μ |
| λΆλ³μ± | Value Objectλ λΆλ³ |
π Best Practices:
- Aggregateλ μκ² μ μ§
- Repositoryλ Rootλ§
- λΉμ¦λμ€ λ‘μ§μ Entityμ
- Anemic Model μ§μ
- κ³Όλν μΆμν μ§μ
- νκ³Ό μ§μμ μν΅
DDDλ 볡μ‘ν λλ©μΈμ λ€λ£° λ λΉμ λ°νλ€.
μμ νλ‘μ νΈμλ κ³Όν μ μμ§λ§,
ν° νλ‘μ νΈμμλ μ μ§λ³΄μμ±μ ν¬κ² ν₯μμν¨λ€.