DDD(Domain-Driven Design)는 복잡한 소프트웨어 프로젝트에서 도메인(비즈니스 영역)에 집중하여 설계하는 방법론입니다. 단순히 기술적인 구조가 아닌, 비즈니스 로직과 규칙을 중심으로 소프트웨어를 설계합니다.
기존 개발 방식의 문제점
DDD의 해결책
개발자와 도메인 전문가가 같은 용어를 사용하는 것
예시 : 전자상거래
// ❌ 기술적 용어
class UserData {
private String id;
private List<ItemData> cart;
private PaymentInfo payment;
}
// ✅ 도메인 언어
class Customer {
private CustomerId customerId;
private ShoppingCart cart;
private PaymentMethod paymentMethod;
}
비즈니스 규칙과 로직을 표현하는 객체들
도메인 모델이 적용되는 명확한 경계
고유한 식별자를 가지며 생명주기 동안 연속성을 유지하는 객체
public class Order {
private OrderId orderId; // 고유 식별자
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private LocalDateTime orderDate;
public Order(OrderId orderId, CustomerId customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING;
this.orderDate = LocalDateTime.now();
}
// 비즈니스 로직
public void addItem(Product product, int quantity) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("확정된 주문은 수정할 수 없습니다");
}
items.add(new OrderItem(product, quantity));
}
public void confirm() {
if (items.isEmpty()) {
throw new IllegalStateException("주문 항목이 없습니다");
}
this.status = OrderStatus.CONFIRMED;
}
}
식별자가 없고 값으로만 구분되는 불변 객체
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("금액은 음수일 수 없습니다");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("통화가 다릅니다");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// equals, hashCode 구현 필수
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Money money = (Money) obj;
return Objects.equals(amount, money.amount) &&
Objects.equals(currency, money.currency);
}
}
연관된 엔티티와 값 객체들의 집합으로 일관성 경계를 정의
public class Order { // Aggregate Root
private OrderId orderId;
private CustomerId customerId;
private List<OrderItem> orderItems; // 내부 엔티티
private ShippingAddress shippingAddress; // 값 객체
private OrderStatus status;
// 애그리게이트 내부의 일관성 보장
public void changeShippingAddress(ShippingAddress newAddress) {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("배송된 주문의 주소는 변경할 수 없습니다");
}
this.shippingAddress = newAddress;
}
// 외부에서는 Aggregate Root를 통해서만 접근
public BigDecimal getTotalAmount() {
return orderItems.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
애그리게이트의 저장과 조회를 담당하는 인터페이스
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId orderId);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findByStatus(OrderStatus status);
}
// 구현체는 인프라스트럭처 계층에
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public void save(Order order) {
entityManager.persist(order);
}
@Override
public Optional<Order> findById(OrderId orderId) {
return Optional.ofNullable(
entityManager.find(OrderEntity.class, orderId.getValue())
).map(this::toDomain);
}
// Entity ↔ Domain 변환 로직
private Order toDomain(OrderEntity entity) {
// 변환 로직
}
}
여러 애그리게이터에 걸친 비즈니스 로직을 처리
@Service
public class OrderPricingService {
public Money calculateTotalPrice(Order order, Customer customer) {
Money itemsTotal = order.getItemsTotal();
Money discount = calculateDiscount(customer, itemsTotal);
Money shippingFee = calculateShippingFee(order.getShippingAddress());
return itemsTotal.subtract(discount).add(shippingFee);
}
private Money calculateDiscount(Customer customer, Money total) {
if (customer.isVip()) {
return total.multiply(0.1); // 10% 할인
}
return Money.ZERO;
}
private Money calculateShippingFee(ShippingAddress address) {
// 지역별 배송비 계산 로직
return new Money(BigDecimal.valueOf(3000), Currency.KRW);
}
}
DDD는 보통 헥사고날 아키텍처와 함께 사용됩니다.
┌─────────────────────────────────────┐
│ Infrastructure │
│ (Database, External APIs, etc.) │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ Application │
│ (Use Cases, Services) │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ Domain │
│ (Entities, VOs, Aggregates) │
└─────────────────────────────────────┘
DDD는 복잡한 비즈니스 도메인을 다루는 프로젝트에서 진가를 발휘합니다. 단순한 CRUD 애플리케이션에서는 과할 수 있지만, 복잡한 비즈니스 규칙이 있는 시스템에서는 코드의 가독성과 유지보수성을 크게 향상시킵니다.