
취미로 진행 중인 사이드 프로젝트 커머스 플랫폼(pick-me)을 DDD로 설계하면서 느낀 점을 정리했다.
DDD(Domain-Driven Design) 전술적 패턴을 처음 공부할 때 묘한 기시감이 들었다.
처음에는 "DDD가 OOP를 다른 이름으로 포장한 건가?" 싶었다. 하지만 실제로 코드를 작성해 보니 같은 문제를 다른 관점에서 바라보는 것이었다. OOP가 "좋은 코드 구조"를 추구한다면 DDD는 "좋은 코드 구조가 비즈니스 문제를 정확히 반영하는가"를 추구한다.
이 글에서는 pick-me 프로젝트의 실제 코드를 통해 DDD 패턴을 적용하다 보면 왜 자연스럽게 좋은 객체지향 코드가 되는지 이야기해 본다.
1. "주문 취소"를 어디에 작성할 것인가
프로젝트에서 가장 먼저 부딪힌 질문이다. 주문 취소 로직을 어디에 넣을까?
### 처음에 작성하기 쉬운 코드

동작은 한다. 하지만 이 코드에는 문제가 있다.
"PLACED 상태에서만 취소 가능하다"는 비즈니스 규칙이 Service에 흩어져 있다. 주문 취소를 호출하는 곳이 10군데라면, 10군데 모두 같은 if문을 작성해야 한다. 하나라도 빠뜨리면 PAID 상태인 주문이 취소된다.
### DDD가 제안하는 코드

이제 Service는 `order.cancel("고객 요청")` 한 줄만 호출한다. "어떤 상태에서 취소 가능한가"는 Order가 안다. Service가 알 필요가 없다.
이것이 DDD에서 말하는 Rich Domain Model이고 OOP에서 말하는 캡슐화다. 같은 코드를 놓고 DDD는 "도메인 규칙이 도메인 안에 있다"라고 설명하고 OOP는 "상태와 행위가 하나의 객체에 묶여 있다"라고 설명한다. 설명하는 언어가 다를 뿐 코드는 같다.
2. 금액을 long으로 쓸 것인가, Money로 쓸 것인가
주문 금액을 `long totalAmount`로 선언하면 아무 문제없어 보인다. 하지만 이런 코드가 생기기 시작한다.

금액이 음수가 되면? 통화 단위가 다르면? 이런 검증이 모든 계산 로직에 반복된다.
### DDD가 제안하는 코드 — Value Object

`Money`를 만들면 "금액은 0 이상"이라는 규칙이 Money 안에 한 번만 존재한다. 어디서 Money를 사용하든 음수 금액은 만들어지지 않는다.
같은 패턴이 다른 곳에서도 반복된다.

DDD에서는 이것을 Value Object라 부른다. OOP에서는 불변 객체라 부른다. Java의 `String`, `Integer`, `BigDecimal`이 이미 이 패턴이다. DDD가 새로운 개념을 만든 게 아니라 "도메인의 값을 타입으로 명시하라"는 기준을 준 것이다.
Entity(주문)는 식별자가 있어서 "ORD-001과 ORD-002는 다른 주문"이지만 Money(금액)는 식별자가 없어서 "29,900원은 어디서든 29,900원"이다.
3. Order의 내부를 밖에서 건드리면 안 되는 이유
Order에는 `List<OrderLine> orderLines`가 있다. 만약 외부에서 이 리스트를 직접 수정할 수 있다면?

총금액은 그대로인데 주문 항목이 하나 추가된다. 데이터 정합성이 깨진다.
### 해결: Aggregate


DDD에서는 이것을 Aggregate(집합체)라 부른다. Order가 Aggregate Root이고 외부에서는 반드시 Order를 통해서만 내부 상태에 접근한다.
예를 들어 Aggregate Root에게 명령을 아래처럼 내리면 된다. 외부는 의도만 전달하고 비즈니스 규칙은 엔티티가 책임진다.

OOP에서는 이것을 정보 은닉이라 부른다. private 필드를 외부에 직접 노출하지 않는 원칙이다. DDD의 Aggregate는 이 원칙을 단일 객체가 아니라 관련 객체 군집 단위로 확장한 것이다.
4. 도메인은 데이터베이스를 모른다
Order를 DB에 저장해야 한다. 가장 쉬운 방법은 Order에 JPA 어노테이션을 붙이는 것이다.

하지만 이러면 Order가 JPA라는 인프라스트럭처에 의존한다. DB를 바꾸면 도메인 코드도 바꿔야 한다.
### 해결: Repository (Port/Adapter)


도메인의 `Order.java`에는 `@Entity`, `@Column`이 없다. JPA 전용 엔티티(`OrderJpaEntity`)는 별도로 존재하고, `OrderMapper`가 둘 사이를 변환한다.
이 프로젝트에서는 ArchUnit으로 이 규칙을 CI에서 강제한다.

DDD에서는 이것을 도메인 순수성이라 부르고, OOP/SOLID에서는 의존성 역전 원칙(DIP)이라 부른다. 의존 방향이 항상 바깥(infrastructure) → 안쪽(domain)을 향한다.
5. new Order()가 아니라 Order.place()인 이유

이 코드만 보면 이 Order가 "새로 접수된 주문"인지, "DB에서 복원된 주문"인지 알 수 없다.

`place`라는 이름이 "고객이 주문을 접수하는 행위"라는 비즈니스 맥락을 전달한다. 프로젝트 전체에서 이 패턴이 일관된다.

GoF에서는 이것을 팩토리 메서드 패턴이라 부른다. DDD는 여기에 유비쿼터스 언어(Ubiquitous Language) — 도메인 전문가와 개발자가 공유하는 비즈니스 용어 —라는 맥락을 더한다. OOP에서 "생성 로직의 캡슐화"를 추구한다면, DDD에서는 "생성 행위에 비즈니스 의미를 부여"한다.
6. Entity에 넣기 어색한 로직은 어디에 두는가
결제 처리를 생각해 보자. PG사를 호출하고, 결과에 따라 Payment 상태를 바꿔야 한다.
이 로직을 Payment Entity에 넣으면?

Application Service에 넣으면?

### 해결: Domain Service

Application Service는 이 Domain Service를 호출만 한다.

Domain Service는 DB 저장도 안 하고, 트랜잭션도 안 걸고, 이벤트 발행도 안 한다. 순수하게 "PG 호출 + 결과에 따른 상태 전이"라는 비즈니스 규칙만 담당한다.
OOP에서는 이것을 단일 책임 원칙(SRP)으로 설명한다. "이 로직이 이 클래스에 있어야 하나?"라는 질문에 대한 답이 Domain Service다.
7. 주문이 접수되면 재고가 차감되어야 한다
Order와 Inventory는 다른 Bounded Context다. Order가 Inventory를 직접 호출하면 강한 결합이 생긴다.

### 해결: Domain Event

핵심은 이벤트를 Aggregate 내부에서 생성한다는 것이다.

"주문이 접수되었다"는 사실은 Order가 가장 잘 안다. Service에서 `new OrderPlacedEvent(...)`를 만들면 도메인 지식이 Service로 누수된다.
Application Service는 이벤트를 Outbox에 저장하는 발행 메커니즘만 담당한다.

GoF에서는 이것을 옵저버 패턴이라 부른다. DDD에서는 Domain Event라 부른다. "누가 듣고 있는지 모르지만, 일어난 사실을 알려준다"는 원칙이 같다.
8. 7개 서비스에 같은 코드가 반복된다면
처음에 이벤트 발행 코드가 7개 서비스에 복제되어 있었다.

### 해결: 인터페이스 추출

`DomainEventPublisher`는 Order인지 Payment인지 Stock인지 알 필요가 없다. `getDomainEvents()`를 호출할 수 있는 객체면 된다. 이것이 다형성이다.

9. 재고 차감을 누가 판단하는가
마지막으로, OOP에서 가장 중요한 원칙 하나.

`stock.reserve()`의 내부를 보면:

외부에서는 `stock.reserve(2, orderId)` 한 줄이다. 수량 검증, 재고 차감, 예약 증가, 부족 이벤트, 소진 이벤트 — 이 모든 것이 Stock 안에 있다.
OOP에서는 이것을 Tell, Don't Ask 원칙이라 부른다. 객체에게 상태를 물어보고 내가 판단하지 말고, 행위를 시켜라. DDD의 Rich Domain Model은 이 원칙의 자연스러운 결과다.
그래서, DDD는 왜 객체지향 같을까?
DDD가 OOP를 베낀 게 아니다. DDD는 "비즈니스 문제를 코드로 정확히 표현하려면 어떻게 해야 하는가?"를 고민한 결과이고 OOP는 "변경에 유연하고 이해하기 쉬운 코드 구조는 무엇인가?"를 고민한 결과다.
두 질문의 답이 겹치는 이유는 비즈니스 규칙을 가장 잘 아는 객체가 그 규칙을 담당하는 것이 결국 캡슐화이고, 정보 은닉이고, 단일 책임이기 때문이다.
DDD를 모르고 OOP를 잘해도 캡슐화를 철저히 지키면 Rich Model이 나온다. OOP를 모르고 DDD 패턴만 따라 해도 자연스럽게 캡슐화가 되고, SRP가 지켜지고, DIP가 적용된다.
결국 좋은 코드는 같은 곳에 수렴한다. 1:1로 매핑하는 건 무리가 있을 수 있지만, DDD와 OOP는 서로 다른 출발점에서 같은 코드에 도달하는 두 개의 길이다.
출처
https://www.youtube.com/watch?v=dJ5C4qRqAgA
https://tech.kakaopay.com/post/backend-domain-driven-design/
카카오페이 여신코어 DDD(Domain Driven Design, 도메인 주도 설계)로 구축하기 | 카카오페이 기술 블로
카카오페이의 여신업무 내재화 프로젝트를 DDD를 적용하여 구축한 내용을 공유하고자 합니다.
tech.kakaopay.com
레거시 시스템을 DDD(도메인 주도 설계) 기반으로 재설계한 이야기 1편
https://liasn.tistory.com/10 솔루션 업체 개발자에서 교육 스타트업으로 이직 회고(with. F-Lab 멘토링)▶ 들어가며 코로나 때문에 불안한 취업 시장에서 퇴사와 이직을 결심하고 그 끝에 만족스러운 결
liasn.tistory.com
https://techblog.lycorp.co.jp/ko/applying-ddd-to-merchant-system-development
DDD를 Merchant 시스템 구축에 활용한 사례를 소개합니다
이제는 꽤나 대중적인 방법론으로 자리 잡은 DDD(domain driven design)는 도메인을 중심으로 소프트웨어를 모델링하는 데 중점을 둔 설계 접근 방식입니다.저는 DDD...
techblog.lycorp.co.jp
'Dev' 카테고리의 다른 글
| Kafka 간 맞추기 (1) | 2026.04.20 |
|---|---|
| 샤갈!! 저 Temporal Workflow 엔진 도입했어요!! (0) | 2026.04.12 |
| 와 CDC(Change Data Capture)! Debezium 로그 기반 아시는구나! (0) | 2026.04.08 |
| Kafka 간보기 (1) | 2026.04.07 |
| 기술 면접에서 털리고 쓰는 동시성 제어와 멱등성 (0) | 2026.03.27 |