DDD(Domain Driven Design)와 Spring 적용 — 복잡한 비즈니스를 코드로 명확하게 표현하기

대규모 시스템에서 가장 어려운 문제 중 하나는 비즈니스 로직의 복잡성을 코드로 일관성 있게 표현하는 것입니다. 이를 해결하기 위해 제안된 접근 방식이 바로 DDD(Domain Driven Design, 도메인 주도 설계)입니다. 

 

 

1. DDD란 무엇인가?

DDD는 도메인(비즈니스 영역)을 중심으로 소프트웨어를 설계하는 방법론입니다. 단순히 기술 구조를 나누는 것이 아니라, 실제 비즈니스 개념과 언어(Ubiquitous Language)를 코드에 반영하여 개발자와 도메인 전문가가 동일한 언어로 소통할 수 있게 합니다.

  • 도메인(Domain): 비즈니스가 해결하고자 하는 문제의 영역
  • 모델(Model): 도메인을 코드로 표현한 추상화된 개념
  • Ubiquitous Language: 팀 전체가 공유하는 통일된 비즈니스 용어
  • Bounded Context: 하나의 모델이 유효하게 동작하는 경계 (서브 도메인 단위)

DDD의 핵심 목표는 “복잡한 비즈니스를 단순하고 명확하게 표현”하는 것입니다.

 

2. DDD의 기본 구성 요소

DDD의 설계는 도메인 모델을 중심으로 구성됩니다. 주요 구성 요소는 다음과 같습니다.

  • Entity: 고유 식별자(ID)를 가지며, 상태가 변할 수 있는 객체
  • Value Object: 식별자 없이 값으로 동등성을 비교하는 객체 (예: Money, Address)
  • Aggregate: 연관된 엔티티(Value Object 포함)를 하나의 단위로 묶은 루트 구조
  • Repository: Aggregate를 저장/조회하는 인터페이스
  • Service: 비즈니스 로직을 수행하는 도메인 서비스 또는 애플리케이션 서비스
  • Event: 도메인 상태 변화가 발생했음을 알리는 메시지

Spring Boot는 이러한 DDD의 구조를 자연스럽게 지원할 수 있는 아키텍처를 제공합니다.

 

3. Spring에서의 DDD 계층 구조

Spring 애플리케이션은 다음과 같은 DDD 구조로 설계할 수 있습니다.

src
 ├── domain
 │    ├── model          # Entity, Value Object, Aggregate
 │    ├── repository     # Repository Interface
 │    └── service        # Domain Service
 ├── application
 │    └── service        # Use Case (비즈니스 흐름)
 ├── infrastructure
 │    ├── persistence    # JPA, MyBatis 구현체
 │    ├── event          # 메시징, Kafka, Redis 등
 │    └── config         # 환경설정
 └── interfaces
      └── api            # Controller, DTO, Request/Response

각 계층은 명확한 역할을 가지고 있으며, 의존 방향은 도메인 → 애플리케이션 → 인프라스트럭처 순으로 향합니다. 즉, 도메인은 외부 기술에 의존하지 않아야 합니다.

 

4. Entity와 Value Object 구현 예시

Entity

@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Money totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    protected Order() {}

    public Order(Money totalAmount) {
        this.totalAmount = totalAmount;
        this.status = OrderStatus.PENDING;
    }

    public void pay() {
        if (status != OrderStatus.PENDING) throw new IllegalStateException("이미 결제된 주문입니다.");
        this.status = OrderStatus.PAID;
    }
}

Value Object

@Embeddable
public class Money {

    private BigDecimal amount;

    protected Money() {}

    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 boolean isGreaterThan(Money other) {
        return this.amount.compareTo(other.amount) > 0;
    }
}

Money는 식별자가 필요하지 않으며, 값 자체로 동등성을 판단하는 불변 객체입니다.

 

5. Repository와 Application Service

Repository 인터페이스

public interface OrderRepository {
    Optional<Order> findById(Long id);
    Order save(Order order);
}

Application Service (Use Case)

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;

    @Transactional
    public void payOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
        order.pay();
        orderRepository.save(order);
    }
}

Application Service는 트랜잭션 관리 및 도메인 서비스 호출을 담당하며, 비즈니스 흐름의 진입점 역할을 합니다.

 

6. 도메인 이벤트와 비동기 처리

도메인 내에서 상태 변화가 발생했을 때, 다른 컨텍스트에 알려야 할 경우 도메인 이벤트를 사용합니다.

public class OrderPaidEvent {
    private final Long orderId;
    private final LocalDateTime paidAt;

    public OrderPaidEvent(Long orderId) {
        this.orderId = orderId;
        this.paidAt = LocalDateTime.now();
    }
}
@Component
public class OrderEventHandler {

    @EventListener
    public void handleOrderPaid(OrderPaidEvent event) {
        System.out.println("주문 결제 완료: " + event.getOrderId());
    }
}

이벤트 기반 설계를 통해 서비스 간 결합도를 낮추고, 마이크로서비스 간 통합에도 유용하게 적용할 수 있습니다.

 

7. DDD + Spring 실무 적용 포인트

  • JPA Entity는 도메인 모델이지만 영속성에 종속되지 않도록 설계 — 비즈니스 로직 중심 구조 유지
  • 서비스 간 경계(Bounded Context)를 명확히 정의 — 주문, 결제, 배송, 회원 등 독립적인 도메인으로 분리
  • DTO와 도메인 객체는 구분 — API 계층에서만 DTO 사용, 내부 로직은 Domain 객체로 처리
  • 애그리게잇 간 직접 참조 금지 — ID로만 관계를 유지하여 결합도 최소화
  • 도메인 이벤트 기반 통합 — 동기 호출보다 이벤트 발행으로 컨텍스트 간 협력

 

8. DDD의 장점과 주의할 점

장점

  • 복잡한 비즈니스 로직을 코드로 명확히 표현
  • 도메인 전문가와의 커뮤니케이션 비용 감소
  • 유지보수성, 확장성이 뛰어난 구조
  • 테스트 가능한 아키텍처 확보

주의할 점

  • 초기 설계와 모델링에 많은 시간 필요
  • 도메인 경계를 명확히 정의하지 않으면 오히려 복잡도 증가
  • 작은 규모의 프로젝트에는 과도할 수 있음

 


 

DDD는 단순한 설계 패턴이 아니라 비즈니스 중심의 사고방식입니다. Spring Boot의 계층 구조, JPA, 이벤트 시스템을 적절히 활용하면 도메인 모델을 자연스럽게 코드에 녹여낼 수 있습니다.