DDD 설계 - KimGyuBek/Threadly GitHub Wiki

도메인 주도 설계

개요

Threadly는 단순한 CRUD 중심의 데이터 처리 서비스가 아니라, 비즈니스 규칙이 코드 구조로 직접 드러나는 도메인 중심 아키텍처를 목표로 설계되었다.

이를 위해 DDD의 핵심 개념인 Bounded Context, Aggregate, Domain 모델 분리를 적용했다.

Threadly는 다음 원칙을 따른다.

  • 도메인 중심으로 기능을 분리한다.
  • 모델은 Aggregate 단위로 구성한다.
  • 기술적 구현(DB, Framework)은 도메인 규칙을 보조하는 수단으로 두며, 외부 의존성과 완전히 격리한다.

이 구조는 단순한 계층 분리나 코드 정리를 넘어,

"도메인이 시스템의 중심에 있어야 한다." 는 철학을 구현한다.

그 결과 Threadly는 비즈니스 로직의 일관성, 낮은 결합도, 높은 응집도를 동시에 유지하는 견고한 도메인 기반 구조를 갖추게 되었다.


CURD 방식과의 비교

기존 대부분의 프로젝트는 DB 테이블을 먼저 만들고, 그 구조를 기반으로 API를 구현하는 방식의 데이터 중심 설계로 시작한다.

단순 CRUD에는 적합하지만, 도메인이 복잡해질수록 한계가 드러난다.

비즈니스 로직이 분산되고 외부 인프라에 종속되어 테스트와 변경이 어렵다. 또한 엔티티가 단순히 DB 구조를 반영하는 의미 없는 데이터 객체로 전락한다.

반면, DDD 설계는 비즈니스 규칙을 도메인 모델에 집중하고, 헥사고날 아키텍처로 핵심 로직을 외부 기술로 격리한다. 또한 Bounded Context로 도메인 간 책임을 명확히 분리한다.

결과적으로 Threadly는 데이터 중심이 아닌 도메인 규칙 중심 구조로서 복잡성을 줄이고 유지보수성을을 높였다.


도메인 아키텍쳐 전체 구조

ddd


도메인 모델 설계 원칙

1. 순수 Java 객체

core-domain 모듈은 외부 기술(JPA, Spring 등)에 전혀 의존하지 않는 순수 Java 객체로 구성된다.

또한 도메인 로직은 외부 기술의 제약 없이 비즈니스 규칙 그 자체를 표현한다.

  • core-domain 모듈은 어떤 계층에도 의존하지 않는다.
  • 테스트가 가능하고 재사용 가능한 비즈니스 로직 중심 구조를 유지한다.
  • 외부 환경의 변화(JPA, DB의 교체 등)에도 영향을 받지 않는다.

2. 비즈니스 규칙 캡슐화

  • 도메인 객체는 단순한 데이터 보관용 DTO가 아니라, 해당 도메인의 핵심 규칙을 스스로 수행하는 객체다.

모든 비즈니스 행위(생성, 수정, 삭제 등)는 반드시 도메인 내부 메서드를 통해 수정된다.

예시:

- `User` -> `UserProfile` 생성 책임을 내부에서 수행(`User.createProfile()`)
- `Post` -> `PostComment` 생성을 내부에서 수행(`Post.newComment()`)

이 규칙을 통해 도메인 규칙이 ServiceController 등 외부로 새어 나아기 않고, 한 곳에서 응집적으로 관리된다.

팩토리 메서드 패턴

도메인 객체 생성 시 외부에서 직접 생성자를 호출하지 않고, 도메인 내부에서 유효성 검증과 비즈니스 규칙을 포함한 팩터리 메서드를 통해 생성한다.

예시:

//User.java
/*새로운 사용자 생성*/
public static User newUser(String userName, String password, String email, String phone) {
  return
      User.builder()
          .userId(RandomUtils.generateNanoId()) //userId를 NanoId로 설정
//            생략
          .userRoleType(UserRoleType.USER) //기본 Role 설정
          .userStatus(UserStatus.INCOMPLETE_PROFILE) //신규 가입 상태 지정
          .build();
}

이 방식을 사용하면면 불완전한 상태의 객체 생성을 방지하고 도메인 규칙을 따르는 일관성 있는 생성을 보장한다.

값 객체(VO)

비즈니스 규칙상 값으로만 식별되는 불변 객체를 recordenum으로 구현한다.

예시:

//FollowStatus.java
public enum FollowStatus {
  NONE, // 팔로우 요청도 안 한 상태
  PENDING, //팔로우 요청이 수락 대기 중인 상태 (비공개 계정 대상)
  APPROVED, // 팔로우 요청이 수락된 상태 (팔로잉 관계 성립)
  SELF // 내 프로필인 경우
}

이들은 식별자 보다 의미와 상태 일관성을 보장하는데 중점을 둔다.

3. JPA Entity 분리

  • JPA Entity는 단순히 DB 저장을 위한 구조이며, 비즈니스 로직은 모두 도메인 객체(core-domain)에 존재한다.
  • 도메인과 인프라를 완전히 분리해 순수한 도메인 모델으르 유지하고, 테스트/확장성/이식성을 극대화한다.
  • 두 계층은 Mapper를 통해 명시적으로 변환된다.

상세 문서:

4. 도메인 규칙 우선 설계

엔드포인트 경로나 데이터베이스 테이블 구조보다 도메인의 구칙과 책임이 우선되어야 한다.

즉 설계 결정의 기준은 "데이터 구조"가 아니라 "도메인의 의미"여야 한다.

관련 문서: DDD 관점의 책임 결정


Bounded Context 설계

Bounded Context는 다음과 같은 특징을 가진다.

  • Context는 자체 도메인 모델과 책임을 갖고, 서로의 내부 구현에 의존하지 않는다.
  • Context간 협력은 도메인 이벤트 또는 Port 인터페이스로만 이뤄진다.
  • Context는 독립 모델을 유지하고, 다른 Context의 내부 구현을 직접 참조하지 않는다.
  • 경계 밖에서는 **의미(사실)**만 교환하며, 내부 구현 디테일은 숨긴다.

Aggregate

Aggregate는 항상 함께 관성이 유지되어야 하는 도메인 객체들의 집합이다.

각 도메인의 모든 내부 변경은 반드시 Aggregate Root를 통해서만 이루어져야 한다.

즉, Aggregate는 관련 객체를 하나의 단위로 묶어 응집도와 캡슐화를 유지한다.

Root Aggregate를 통한 일관성 관리

  • Aggregate 내부 상태 변경은 Root Aggregate를 통해서만 가능하다.
  • 외부에서 Root를 건너뛰고 내부 엔티티(JPA Entity와 별도인 도메인 객체)에 직접 접근할 수 없다.
  • 모든 비즈니스 규칙은 Root Aggreagte 내부에서 수행된다.

예시:

//직접 호출 금지
postLike.newLike(...) 
userProfile.newProfile(...) 

//Root Aggregate 내부 로직에서 처리
post.addLike(...)
user.setProfile(...)
//Post.java
public PostComment addComment(String userId, String comment) {
  return
      PostComment.newComment(this.postId, userId, comment); //PostComment의 내부 로직 호출
}

이처럼 Aggregate 내부의 규칙은 Root Aggregate가 관리하여 무결성과 응집도를 보장한다.

Aggreagte간 연결

Aggregate간의 연결은 다른 Aggregate 내부를 직접 참조하지 않고 식별자 ID로만 이루어진다.

예시:

//PostComment.java
public class PostComment {

  private String commentId;
  private String postId; //postId를 통해서 Post 참조
  /*생략*/
}

이 방식은 Aggregate간의 강한 결합을 방지하고, 각 비즈니스 단위의 확장성과 독립성을 높인다.

동일한 ContextAggregate간 접근 제한

예시:

  • PostPostComment는 같은 Post Context 속하지만 서로 다른 Aggregate이다.
  • Post는 게시글 자체의 행위와 댓글 생성 허용/검증까지만 관여한다.
  • 게시글 댓글의 수정/삭제/좋아요(취소)등 댓글의 생명주기는 PostComment Aggregate가 직접 전담하며 Post Aggregate에서 PostComment 내부 로직에 접근하는 것은 금지 된다.
post.addComment(...) // 게시글 댓글 생성 책임은 Post
postComment.like(...) //댓글 좋아요는 PostComment Aggregate의 책임
postComment.markAsDeleted(...) // 댓글 삭제도 PostComment Aggregate의 책임

즉, 같은 Context 내에서도 Aggregate간에는 ID로먼 연결하고,

서로의 내부 로직에는 접근하지 않는다.

이를 통해 Aggregate 경계의 독립성과 응집도를 유지한다.


관련 문서

⚠️ **GitHub.com Fallback** ⚠️