DDD 아키텍처, 왜 요즘 다시 주목받고 있을까?

2026. 7. 10. 21:20Dev & Tech/Etc

반응형

DDD 아키텍처, 왜 요즘 다시 주목받고 있을까?

서비스가 커지고 비즈니스 로직이 복잡해질수록, "이 코드가 대체 무슨 일을 하는 코드인지" 파악하는 데 점점 더 많은 시간이 걸리는 경험을 다들 한 번쯤 해보셨을 겁니다. 컨트롤러 하나에 검증 로직, 계산 로직, 저장 로직이 뒤섞여 있고, 새 기능 하나를 추가하려면 서비스 클래스 전체를 다시 읽어야 하는 상황 말이죠. DDD(Domain-Driven Design, 도메인 주도 설계)는 바로 이 문제를 풀기 위해 등장한 설계 방법론입니다.

이번 글에서는 DDD가 정확히 무엇이고 왜 다시 주목받고 있는지부터, 핵심 개념과 실제 계층 구조, 그리고 도입할 때 흔히 하는 실수까지 개발자 입장에서 실용적으로 정리해 보겠습니다.
목차
1. DDD란 무엇이고 왜 등장했을까
2. DDD의 핵심 개념 정리
3. 계층형 아키텍처로 보는 DDD 구조
4. 실전 적용 시 주의할 점과 장단점

#1. DDD란 무엇이고 왜 등장했을까
1) DDD의 정의와 배경
DDD는 2003년 에릭 에반스(Eric Evans)가 저서 《도메인 주도 설계》를 통해 처음 체계화한 개념입니다. 핵심 아이디어는 단순합니다. 소프트웨어의 구조를 기술이 아니라 비즈니스 도메인(업무 영역) 중심으로 설계하자는 것입니다. 데이터베이스 테이블 구조나 프레임워크의 편의성보다, 실제 업무 전문가가 이해하는 개념과 용어를 코드에 그대로 반영하는 데 초점을 맞춥니다.

이 책이 나온 지 20년이 넘었지만, 다루는 문제의식 자체는 여전히 유효합니다. 기술은 계속 바뀌어도 비즈니스 로직의 복잡성이라는 문제는 사라지지 않기 때문입니다.
(1) 전통적인 계층형 설계와의 차이
전통적인 방식은 Controller-Service-Repository로 계층을 나누되, 정작 Service 클래스 안에는 온갖 로직이 절차적으로 나열되는 경우가 많습니다. 반면 DDD는 도메인 객체 스스로가 자신의 상태를 검증하고 행위를 수행하도록 설계합니다. 데이터와 로직이 분리되지 않고 한 곳에 응집된다는 점이 가장 큰 차이입니다.
· · · · ·
2) 왜 최근 다시 주목받고 있을까
DDD 자체는 20년이 넘은 개념이지만, 최근 몇 년 사이 다시 많이 언급되는 데는 몇 가지 배경이 있습니다.
마이크로서비스 아키텍처(MSA)의 확산 - 서비스를 어떤 기준으로 쪼갤지 고민할 때, DDD의 '바운디드 컨텍스트' 개념이 자연스러운 경계 기준으로 활용됩니다.

비즈니스 로직의 복잡도 증가 - 단순 CRUD를 넘어서는 복잡한 정책과 규칙이 늘어나면서, 이를 코드로 명확하게 표현할 방법이 필요해졌습니다.

유지보수 비용에 대한 인식 변화 - 초기 개발 속도보다 장기적인 유지보수성이 더 중요하다는 공감대가 커졌습니다.
(1) 모든 프로젝트에 필요한 것은 아니다
다만 DDD가 만능 해법은 아닙니다. 단순한 게시판이나 어드민 페이지처럼 도메인 복잡도가 낮은 프로젝트에서는 오히려 과도한 설계가 될 수 있습니다. 이 부분은 4장에서 좀 더 자세히 다루겠습니다.

#2. DDD의 핵심 개념 정리
1) 유비쿼터스 언어(Ubiquitous Language)
개발자와 기획자, 업무 전문가가 모두 같은 용어를 같은 의미로 사용하는 공통 언어를 뜻합니다. 예를 들어 기획자는 '주문 취소'라고 부르는데 코드에서는 'deleteOrder'라고 되어 있다면, 이 간극이 소통 비용과 버그의 원인이 됩니다. DDD에서는 이런 용어 불일치를 최소화하는 것을 매우 중요하게 다룹니다.
· · · · ·
2) 바운디드 컨텍스트(Bounded Context)
하나의 큰 시스템을 의미 있는 단위로 나눈 경계를 말합니다. 예를 들어 '주문'이라는 개념도 '주문 관리' 컨텍스트에서는 배송 상태가 중요하지만, '정산' 컨텍스트에서는 금액과 세금 처리가 중요합니다. 같은 단어라도 컨텍스트에 따라 다른 의미와 속성을 가질 수 있다는 것을 인정하고, 이를 명확히 구분하는 것이 핵심입니다.
· · · · ·
3) 엔티티와 값 객체
DDD에서 도메인 모델을 구성하는 가장 기본적인 두 가지 요소입니다.
(1) 엔티티(Entity)
고유한 식별자(ID)를 가지고, 상태가 변해도 동일성이 유지되는 객체입니다. 회원, 주문, 상품 등이 대표적입니다.
// 엔티티 예시: 주문(Order)
public class Order {
  private final OrderId id;
  private OrderStatus status;
  private final List<OrderLine> orderLines;

  // 도메인 행위: 상태 변경 로직을 객체 스스로 검증
  public void cancel() {
    if (this.status == OrderStatus.SHIPPED) {
      throw new IllegalStateException("배송 시작 후에는 취소할 수 없습니다.");
    }
    this.status = OrderStatus.CANCELLED;
  }
}
(2) 값 객체(Value Object)
식별자가 없고, 속성 값 자체가 같으면 동일한 객체로 취급되는 불변 객체입니다. 주소, 금액, 기간 등이 대표적입니다.
// 값 객체 예시: 금액(Money)
public final class Money {
  private final BigDecimal amount;
  private final Currency currency;

  public Money add(Money other) {
    validateSameCurrency(other);
    return new Money(this.amount.add(other.amount), this.currency);
  }
  // equals/hashCode는 amount, currency 값 기준으로 구현
}
· · · · ·
4) 애그리거트와 리포지토리
애그리거트(Aggregate)는 관련된 엔티티와 값 객체를 하나의 묶음으로 관리하는 단위입니다. 예를 들어 주문(Order)과 주문 항목(OrderLine)들은 함께 생성되고 함께 삭제되므로 하나의 애그리거트로 묶입니다. 이때 외부에서는 반드시 애그리거트 루트(Aggregate Root)를 통해서만 내부 객체에 접근해야 한다는 규칙이 있습니다.
(1) 리포지토리(Repository)
애그리거트를 저장하고 조회하는 역할을 담당하는 인터페이스입니다. 도메인 계층은 리포지토리의 구현체가 JPA인지 MyBatis인지 알 필요가 없습니다. 이런 관심사 분리 덕분에 도메인 로직이 특정 기술에 종속되지 않습니다.
// 도메인 계층에 위치하는 리포지토리 인터페이스
public interface OrderRepository {
  Optional<Order> findById(OrderId id);
  void save(Order order);
}
· · · · ·
5) 도메인 이벤트(Domain Event)
도메인 안에서 의미 있는 일이 발생했음을 나타내는 객체입니다. 예를 들어 주문이 취소되면 재고를 복원하고 사용자에게 알림을 보내야 할 수 있는데, 이 모든 로직을 하나의 메서드에 몰아넣는 대신 '주문 취소됨' 이벤트를 발행하고 각각의 관심사를 별도 리스너가 처리하도록 분리할 수 있습니다.
// 도메인 이벤트 예시
public class OrderCancelledEvent {
  private final OrderId orderId;
  private final LocalDateTime occurredAt;
}

// 엔티티 내부에서 이벤트 등록
public void cancel() {
  // ...상태 변경 로직...
  this.registerEvent(new OrderCancelledEvent(this.id, LocalDateTime.now()));
}
이렇게 분리하면 재고 복원 로직과 알림 발송 로직이 서로 알 필요가 없어져, 각 기능을 독립적으로 테스트하고 수정할 수 있습니다. 마이크로서비스 환경에서는 이 이벤트를 메시지 큐로 발행해 다른 서비스와 비동기로 통신하는 방식으로도 자주 활용됩니다.

#3. 계층형 아키텍처로 보는 DDD 구조
1) 4개 계층으로 나누는 구조
DDD를 실제 코드에 적용할 때는 보통 아래와 같이 4개 계층으로 나눕니다. 계층마다 역할과 책임을 명확히 분리하는 것이 핵심입니다.
계층역할주요 구성 요소
Presentation외부 요청 처리Controller, DTO
Application유스케이스 조율Application Service
Domain핵심 비즈니스 로직Entity, Value Object, Domain Service
Infrastructure기술적 구현Repository 구현체, 외부 API 연동
· · · · ·
2) 각 계층의 코드로 살펴보기
(1) Presentation 계층
HTTP 요청을 받아 애플리케이션 계층에 위임하는 역할만 담당합니다. 비즈니스 로직은 절대 컨트롤러에 두지 않습니다.
@RestController
public class OrderController {
  private final OrderApplicationService orderService;

  @PostMapping("/orders/{id}/cancel")
  public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
    orderService.cancelOrder(new OrderId(id));
    return ResponseEntity.ok().build();
  }
}
(2) Application 계층
하나의 유스케이스(사용자 시나리오)를 조율하는 역할을 담당합니다. 트랜잭션 처리, 도메인 객체 조회, 저장 등을 담당하지만 비즈니스 규칙 자체는 도메인 객체에 위임합니다.
@Service
public class OrderApplicationService {
  private final OrderRepository orderRepository;

  @Transactional
  public void cancelOrder(OrderId id) {
    Order order = orderRepository.findById(id)
      .orElseThrow(() -> new OrderNotFoundException(id));
    order.cancel(); // 실제 검증·상태 변경은 도메인 객체가 담당
    orderRepository.save(order);
  }
}
(3) Domain 계층
2장에서 살펴본 엔티티, 값 객체, 애그리거트가 위치하는 시스템의 핵심 계층입니다. 특정 프레임워크나 데이터베이스 기술에 의존하지 않는 순수한 자바 코드로 작성하는 것이 이상적입니다.
(4) Infrastructure 계층
리포지토리 인터페이스의 실제 구현체, 외부 API 연동, 메시지 큐 발행 등 기술적인 세부 사항을 담당합니다. JPA를 쓰다가 다른 기술로 바꾸더라도 도메인 계층은 영향을 받지 않아야 합니다.
· · · · ·
3) 도메인 서비스는 언제 사용할까
하나의 엔티티만으로 표현하기 애매한 로직, 예를 들어 여러 애그리거트에 걸친 비즈니스 규칙이 있다면 도메인 서비스(Domain Service)로 분리합니다. 다만 남용하면 다시 절차적인 코드로 회귀할 수 있어, 정말 필요한 경우에만 제한적으로 사용하는 것이 권장됩니다.
※ 참고: 본 구조는 에릭 에반스의 《도메인 주도 설계》 및 국내외 다수의 DDD 실무 적용 사례를 종합해 정리한 일반적인 패턴입니다.

#4. 실전 적용 시 주의할 점과 장단점
1) DDD의 장점
비즈니스 로직의 응집도 향상 - 로직이 도메인 객체에 모여 있어 파악과 테스트가 쉬워집니다.

유지보수성 개선 - 요구사항 변경 시 영향 범위가 명확해집니다.

팀 간 소통 비용 감소 - 유비쿼터스 언어 덕분에 기획자와 개발자 간 오해가 줄어듭니다.

마이크로서비스 전환 용이 - 바운디드 컨텍스트가 이미 서비스 분리 기준으로 작동합니다.

테스트 작성이 쉬워짐 - 도메인 로직이 프레임워크에 의존하지 않아 단위 테스트를 순수 자바 코드만으로 작성할 수 있습니다.
· · · · ·
2) 도입 시 흔한 실수
(1) 빈혈 도메인 모델(Anemic Domain Model)
엔티티에 getter/setter만 두고 로직은 전부 서비스 계층에 몰아넣는 경우입니다. 겉모습만 DDD를 흉내 낸 것으로, 실질적인 이점을 거의 얻지 못합니다.
(2) 과도한 추상화
모든 프로젝트에 4계층 구조와 애그리거트, 리포지토리 패턴을 기계적으로 적용하려다 보면, 오히려 단순한 기능조차 여러 파일을 오가며 구현해야 하는 부작용이 생깁니다.
(3) 팀 전체의 합의 부족
일부 개발자만 DDD 원칙을 지키고 나머지는 기존 방식대로 개발하면, 코드베이스 안에 서로 다른 두 가지 설계 철학이 뒤섞이는 상황이 벌어집니다. 도입 전 팀 전체의 합의와 학습이 선행돼야 합니다.
· · · · ·
3) 언제 DDD를 쓰고 언제 피해야 할까
상황DDD 적합도
복잡한 비즈니스 규칙이 많은 도메인(금융, 물류 등)적합
장기간 유지보수가 예정된 대규모 서비스적합
단순 CRUD 위주의 어드민·게시판과도한 설계일 수 있음
빠른 MVP 검증이 최우선인 초기 스타트업신중히 검토 필요
결국 DDD는 목적이 아니라 도구입니다. 도메인 복잡도가 그만큼의 투자를 정당화할 만큼 높은지, 팀이 이 방법론을 유지할 역량과 합의가 있는지를 먼저 판단하는 것이 순서입니다.

마무리
DDD는 새로운 프레임워크나 라이브러리가 아니라, 코드를 비즈니스 언어로 표현하는 사고방식에 가깝습니다. 유비쿼터스 언어로 소통 간극을 줄이고, 엔티티와 값 객체로 로직을 응집시키고, 계층을 명확히 분리해 변경에 강한 구조를 만드는 것이 핵심입니다.

다만 모든 프로젝트에 똑같이 적용해야 하는 정답은 아닙니다. 도메인이 복잡하고 오래 유지보수해야 하는 서비스라면 충분히 투자할 가치가 있지만, 단순한 서비스에 무리하게 적용하면 오히려 생산성을 떨어뜨릴 수 있습니다.

지금 맡고 계신 프로젝트의 도메인 복잡도를 먼저 냉정하게 판단해 보시고, 필요한 부분부터 점진적으로 적용해 보시는 것을 추천드립니다.
긴 글 읽어주셔서 감사합니다.


끝.


반응형