[Spring] 도메인 주도 개발(DDD)을 알아보자

thezz9·2025년 4월 29일
3

개요

소프트웨어를 만들다 보면 이런 경험을 하게 된다.
코드는 잘 돌아가는데, 비즈니스 로직은 점점 이해하기 어려워진다.

특히 시스템이 커질수록,
"이 버튼이 왜 이렇게 동작하는 거야?"
"이 데이터는 어디서 관리돼?"
"왜 주문 취소가 이렇게 복잡하지?"
같은 질문들이 생길 수 있다.

이런 복잡성은 기술 문제라기 보다는 비즈니스 자체가 복잡한 탓이 크다.
도메인 주도 개발(DDD)은 이 문제를 해결하고자 나온 방법론 중 하나이다.


1. DDD란 무엇인가?

DDD(Domain-Driven Design)는 비즈니스 문제를 소프트웨어 안에 반영하기 위한 설계 방법론이다.

"문제를 제대로 이해하고, 그 문제를 코드에 반영하자."

핵심에서 말하는 "문제"란 기술이 아니라 비즈니스다.
주문, 결제, 배송 같은 비즈니스 도메인을 제대로 이해하고 그걸 기반으로 모델을 만든다.
그리고 개발자와 기획자가 같은 언어(유비쿼터스 언어)를 쓰며 시스템을 만들어 나간다.


2. DDD가 어디서 필요한가?

온라인 쇼핑몰을 예로 들어보자면 처음에는 상품 등록, 주문하기와 같은 기본적인 기능만 있으면 된다.
하지만 시간이 지나면 요구사항이 디테일하게 늘어난다.

  • 쿠폰 할인
  • 적립금 사용
  • 부분 취소, 전체 환불
  • 상품 옵션 (색상, 사이즈)
  • 주문 상태별 배송 처리

이때 비즈니스 복잡성을 제대로 모델링하지 않으면 코드는 점점 꼬인다.
처음에는 Order 엔티티 하나로 해결됐지만, 나중에는
Order, OrderItem, Coupon, Refund, Delivery 등이 얽히고 설킨다.
이걸 그냥 테이블 맞춰서 CRUD만 하면?
나중에 코드가 스파게티처럼 꼬여서 아무도 고칠 수 없게 된다.
이런 문제를 해결하고자 할 때 DDD를 적용할 수 있다.

(💡+번외) 스파게티 코드??
현재 알바하고 있는 마트의 포스 프로그램이 떠오른다.
앱카드 결제, 서울페이와 같은 기능들은 요구사항이 늘어남에 따라 추가된 기능일 것이다.
그런데 기능을 단순히 붙여만 놓고, 적절한 예외처리가 되지 않아 그 메뉴에서는 유독 오류 발생이 자주 일어난다. 프로그램이 먹통이 된다거나 갑자기 꺼진다거나..
이런 경우도 스파게티 코드로 인해 발생하는 에러에 대한 적절한 처리를 못 하고 있는 것이 아닐까?


3. DDD의 핵심 개념

1. 도메인 (Domain)

해결하려는 비즈니스 문제 자체

쇼핑몰에서는 상품 등록, 주문, 결제, 배송이 각각 하나의 도메인이다.


2. 모델 (Model)

도메인을 코드로 표현한 것

예를 들어, "주문"이라는 개념을 다루기 위해 Order 엔티티, OrderItem 엔티티를 만든다.
이때 중요한 것은 단순한 데이터 덩어리가 아니라 비즈니스 규칙을 반영하는 살아있는 객체여야 한다.

예시:

public class Order {
    private List<OrderItem> orderItems;
    private OrderStatus status;

    public void cancel() {
        if (status.isShipped()) {
            throw new IllegalStateException("배송된 주문은 취소할 수 없습니다.");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

데이터만 들고 있는 객체가 아니라, 비즈니스 규칙도 포함한다.


public boolean checkStatus(OrderStatus orderStatus) {
	return this.status == orderStatus;
}

이게 내가 만들었던 코드였는데, 위 예시를 봤을 때 예외처리도 해당 도메인 엔티티 내에서 해야하는 것 같다. DDD에 어긋나는 코드다.


3. 유비쿼터스 언어 (Ubiquitous Language)

모두가 사용하는 통일된 언어

"상품", "주문", "결제", "취소" 같은 단어를 개발자, 기획자, 심지어 고객 서비스팀까지 모두 현업에서의 의미로 동일하게 사용해야 한다.
"주문"이란 무엇인가?

  • 상품을 장바구니에 담는 것인가?
  • 결제를 완료한 것인가?
    이 정의가 다르면, 시스템도 꼬인다.
    DDD에선 이 언어를 공통으로 정의하고, 코드에도 똑같이 반영해야 한다.

이 부분은 DDD를 떠나 개발에서 매우 중요한 부분인 것 같다.
심지어 작은 팀 프로젝트에서도 각자의 기능 구현 뒤에 문제가 되는 경우가 있다.
같은 도메인을 서로 다르게 이해하고 있는 경우가 많아 그런 경우가 매우 많은 것 같다.


4. 바운디드 컨텍스트 (Bounded Context)

모델이 유효한 경계

"주문(Order)"이라는 개념도

  • 주문 관리 시스템에서는 "결제 완료된 것"
  • 배송 시스템에서는 "배송을 준비해야 하는 것"
    이렇게 다를 수 있다.
    그래서 모델이 적용되는 범위를 명확히 나눈다.
    컨텍스트마다 독립적으로 모델을 설계하고, 필요할 때는 명시적으로 통신(API, 이벤트)한다.

위에서 말한 유비쿼터스 언어가 제대로 정립되지 않으면, 이 부분에서도 문제가 생기는 것 같다.
특히 협업 과정에서 깃 병합 충돌이 난다거나 하는 경우가 이 바운디드 컨텍스트를 제대로 정하지 않아서 그런 것 같다.


4. DDD를 적용할 때 주의할 점

1. 처음부터 모든 곳에 DDD를 적용하려고 하지 말자

DDD는 장점이 많은 방법론이긴 하지만, 모든 프로젝트에 처음부터 적용해야 하는 것은 아니다.
요구사항이 단순하거나 CRUD만 필요한 시스템이라면 굳이 복잡한 비즈니스 로직을 다루기 위한 DDD의 복잡한 모델링을 도입할 필요가 없다.

처음부터 모든 계층을 나누고, 모든 도메인을 정의하려고 하면 오히려 실질적인 개발 속도가 느려진다. 복잡성이 나타나는 순간부터 DDD를 적용하는 것이 가장 현실적이다.

2. 유비쿼터스 언어를 끝까지 지켜야 한다

DDD에서 가장 중요한 개념 중 하나가 바로 유비쿼터스 언어(Ubiquitous Language)다.
개발자, 기획자, 비즈니스 담당자가 모두 같은 단어를 사용하고, 그 단어의 의미를 정확히 공유하는 것이 핵심이다.
만약 용어가 혼란스럽게 사용되면,

  • 개발자는 '주문'이라고 생각했는데,
  • 기획자는 '장바구니에 담은 것'을 말하고 있었다...

이런 식으로 의사소통 오류가 발생하고,
결국 시스템도 엉망이 된다.

모든 코드, 문서, 대화에서 같은 단어를 같은 의미로 사용해야 한다.
모호함을 없애야 진짜 비즈니스 로직을 제대로 모델링할 수 있다.

3. 도메인 로직은 엔티티 안에 담자

DDD에서는 비즈니스 규칙은 엔티티(Entity)나 밸류 오브젝트(Value Object) 안에 포함시킨다.
Service 클래스가 비즈니스 로직을 모두 처리하는 구조는 "Transaction Script" 스타일에 가깝고, DDD에서는 풍부한 도메인 모델(Rich Domain Model) 을 지향한다.

예를 들어, 주문을 취소하는 로직을 OrderService가 직접 하지 않고, Order 엔티티가 스스로 처리해야 한다.

order.cancel();

이렇게 호출했을 때 Order 내부에서 "이미 배송이 시작된 주문은 취소할 수 없다" 같은 비즈니스 규칙을 검사하는 식이다.
Service는 최대한 얇게 유지하고, 핵심 로직은 엔티티에 몰아야 한다.
이렇게 해야 도메인이 살아 움직이는 것처럼 관리할 수 있다.

4. 바운디드 컨텍스트 간에는 명확하게 통신해야 한다

바운디드 컨텍스트(Bounded Context)란, "이 모델은 이 경계 안에서만 유효하다"는 뜻이다.

예를 들어,

  • 주문(Order) 컨텍스트에서는 "결제 대기 중" 상태
  • 결제(Payment) 컨텍스트에서는 "결제 승인" 상태

이렇게 서로 다른 문맥(Context)에서 주문을 다르게 해석할 수 있다.

이때 중요한 것은 컨텍스트 간의 통신은 반드시 명확하게 이루어져야 한다는 것이다.

  • API 호출
  • 이벤트 발행
  • 메시지 큐 사용

이런 명시적인 방법을 통해
서로의 경계를 존중하며 정보를 주고받아야 한다.

컨텍스트를 무시하고 "그냥 DB 테이블 같이 쓰자" 같은 방식으로 통합하면, 곧 경계가 무너지고 시스템이 혼란에 빠진다.
각 바운디드 컨텍스트는 독립적으로 설계하고, 명확한 계약(Contract)을 통해 통신해야 한다.


마무리

어떠한 설계 방법론이나 모델링 기법도 비즈니스의 복잡함 자체를 마법처럼 없애주진 않을 거라고 생각한다. 비즈니스가 존재하는 한, 복잡성은 항상 따라올 것이다. 그리고 개발에는 늘 trade-off가 존재하기 때문에 모든 방법론에는 장단점이 분명히 존재한다.

다만, 좋은 모델을 만들면 이 복잡함을 제어 가능한 형태로 다듬을 수는 있을 것이다. 이를 위해 많은 선배 개발자들이 고민하고 만들어낸 방법들이 지금 내가 정리하고 있는 DDD 같은 것들 아닐까?

이론적인 개발 공부를 하다보면 사실 추상적인 얘기가 많다. 아직 큰 규모의 프로젝트를 직접 경험하기 어려운 입장에서는 이런 모델이나 개념들을 꼬리에 꼬리를 물며 생각해보기 쉽지 않다. 거의 상상의 영역에 가깝다.
그럼에도 이런 지식들을 계속 공부하는 이유는, 언젠간 나도 이 지식들을 진짜로 써먹어야 할 순간이 왔을 때 득과 실을 스스로 판단할 수 있는 힘을 기르기 위함이다.

profile
개발 취준생

0개의 댓글