DDD, 즉 Domain-Driven Design은 단순히 폴더 구조를 나누는 설계 방식이 아니다.
핵심은 데이터가 아니라 도메인의 행위와 규칙을 중심으로 소프트웨어를 설계하는 것이다.
전통적인 MVC 구조의 아키텍처가 너무 진부하다는 생각이 들었다.
가장 구현의 복잡성은 떨어지는 구조이지만, 다른 좋은 구조는 없을까 고민하던차, DDD구조에 관한 책을 추천받아 읽게 되었고, 객체지향적 사고와, 확장성이 좋은 사고방식이라는 생각이 들어, 도전하고싶은 생각이 들었다.

(대충 진부함을 느끼는 릅신 짤)
하지만 사실은 머릿속에 아무것도 들어있지 않은 감자라는것 .. 🤯🤯🤯🤯
그래도, 똑같은 구조를 통해 또 백엔드 프로젝트를 진행하는것은 의미없다는 생각이 들었다.
미지의 모르는 것들을 탐구하고, 성장해 나가고자 새로운 것들에 도전하고자한다.
일반적인 CRUD 중심 설계에서는 주로 다음과 같은 사고방식을 가진다.
데이터를 저장한다.
데이터를 수정한다.
필드 값을 바꾼다.
setName()
setStatus()
setCount()
즉, 관심사가 대부분 데이터와 필드 변경에 맞춰져 있다.
반면 DDD 관점에서는 다음과 같이 생각한다.
회원이 동호회에 가입한다.
클럽장이 모집을 마감한다.
주문이 취소된다.
게시글이 숨김 처리된다.
초대 링크가 비활성화된다.
DDD에서는 단순히
status값을 변경하는 것이 아니라,
도메인에서 실제로 일어나는 행동을 코드로 표현하려고 한다.
이 도메인은 어떤 행동을 하고,
그 행동에는 어떤 규칙이 있는가?
예를 들어 클럽 도메인이라면 다음과 같이 질문한다.
클럽은 생성될 수 있다.
클럽 소개글은 변경될 수 있다.
클럽장은 모집을 마감할 수 있다.
클럽은 삭제될 수 있다.
삭제된 클럽은 다시 삭제할 수 없다.
그리고 이러한 규칙을 도메인 객체 안에 넣는다.
public void delete() {
if (this.deleted) {
throw new IllegalStateException("이미 삭제된 클럽입니다.");
}
this.deleted = true;
}
즉, DDD에서는 setDeleted(true)가 아니라 delete()라는 의미 있는 행위를 만든다.
DDD에서 일반적으로 사용하는 계층은 다음과 같다.
presentation
application
domain
infrastructure
각 계층의 역할은 다음과 같다.
| 계층 | 역할 |
|---|---|
| Presentation | Controller, Request, Response, Validation |
| Application | UseCase, Application Service, Command, Result, Transaction |
| Domain | Entity, Value Object, Aggregate, Domain Service, Repository Interface |
| Infrastructure | JPA Entity, Spring Data Repository, 외부 API Client, 파일 저장소, 메시지 큐 |

핵심은 다음과 같다.
presentation 에서는 우리가 흔히 아는 컨트롤러
application/service 레이어에서는 전통적인 계층 구조와 다르게, 컨트롤러와 도메인을 이어주는 통로정도의 굉장히 가벼운 책임을 가지는 레이어이다.
Domain 도메인 주도 설계의 가장 중요한 레이어로써 단순히 DB와 매핑되는 Entity를 의미하는것이 아니라 그 데이터와 관련된 기능들도 구현이 되어있는 , 비즈니스 로직이 포함되어있는 레이어이다.
Repository 도메인 레이어의 repository interface를 구현한 구현체이다. 외부 db와의 연결을 담당하는 책임을 가지는 레이어이다.
DIP는 Dependency Inversion Principle의 약자다.
일반적으로 고수준 모듈이 저수준 모듈에 직접 의존하면 변경에 취약해진다.
일반적인 경우에, Application Service가 JPA나 DB 기술에 직접 영향을 받는다.
DDD에서는 이를 다음과 같이 뒤집는다.
Application Service
↓
Repository Interface
↑
Infrastructure Repository Implementation

위의 그림을 보게 되면, 도메인 영역에 존재하는 RuleDiscounter 라는 인터페이스를 Infrastructure영역에서 구현 (implement)하고있는것을 확인할 수 있다.
즉, 하위 영역인 Infrastructure 가 구현되어있지 않아도, 혹은 하위 레이어의 기술스택이나 구현방식이 바뀌어도, 상위 영역인 도메인에서의 변경사항이 매우 적다는 장점을 가진다.
하위 기능을 추상화한 인터페이스는 고수준 모듈에 위치한다.
DDD에서는 특히 외부 기술과 만나는 지점에 DIP를 적용하는 것이 효과적이다.
대표적인 예시는 다음과 같다.
DB Repository
외부 API Client
...
이런 요소들은 기술 변경 가능성이 높다.
따라서 Domain 또는 Application 영역에 인터페이스를 두고,
Infrastructure 영역에서 구현체를 제공하는 방식이 좋다.
DDD의 Domain Layer에는 다음과 같은 구성 요소들이 있다.
Entity는 식별자 ID를 가지는 도메인 객체다.
중요한 점은 Entity가 단순히 DB 테이블과 매핑되는 객체가 아니라는 것이다.
DDD에서 Entity는 도메인 기능과 규칙을 함께 가진다.
예를 들어 Club은 다음과 같은 도메인 행위를 가질 수 있다.
public class Club {
private final ClubId id;
private String name;
private String introText;
private boolean deleted;
public void changeIntroText(String introText) {
this.introText = introText;
}
public void delete() {
if (this.deleted) {
throw new IllegalStateException("이미 삭제된 클럽입니다.");
}
this.deleted = true;
}
}
Entity의 핵심은 다음과 같다.
식별자를 가진다.
상태가 변할 수 있다.
도메인 행위를 가진다.
도메인 규칙을 스스로 지킨다.
Value Object는 식별자를 가지지 않는 객체다.
예를 들어 다음과 같은 객체들이 Value Object가 될 수 있다.
Money
Email
Address
...
Value Object의 핵심 특징은 다음과 같다.
ID가 없다.
값이 같으면 같은 객체로 본다.
불변 객체로 만드는 것이 좋다.
상태 변경 메서드를 제공하지 않는 것이 좋다.
예를 들어 ClubId를 Value Object로 만들 수 있다.
public class ClubId {
private final Long value;
public ClubId(Long value) {
if (value == null || value <= 0) {
throw new IllegalArgumentException("클럽 ID는 양수여야 합니다.");
}
this.value = value;
}
public Long value() {
return value;
}
}
Value object 는 불변객체이다.
식별자를 가지지않는다.
set 메소드를 구현하지 않는다.
Aggregate는 연관된 Entity와 Value Object를 하나로 묶은 단위다.
예를 들어 Club Aggregate는 다음과 같은 객체들을 포함할 수 있다.
Club
├── ClubImage
├── ClubKeyword
└── ClubMember
Aggregate에서 가장 중요한 개념은 Aggregate Root다.
외부에서는 내부 객체를 직접 수정하지 않고,
반드시 Aggregate Root를 통해서만 변경해야 한다.

예를 들어 클럽 대표 이미지를 변경한다고 하자.
나쁜 방식은 다음과 같다.
clubImage.markThumbnail();
좋은 방식은 다음과 같다.
club.changeThumbnail(imageId);
외부에서 ClubImage를 직접 수정하는 것이 아니라,
Club이라는 Aggregate Root가 규칙을 통제해야 한다.
Aggregate Root는 내부 객체들이 항상 정상 상태를 유지하도록 관리한다.
예를 들어 클럽 대표 이미지는 하나만 존재해야 한다는 규칙이 있다고 하자.
public void changeThumbnail(Long imageId) {
boolean exists = images.stream()
.anyMatch(image -> image.id().equals(imageId));
if (!exists) {
throw new IllegalArgumentException("클럽 이미지를 찾을 수 없습니다.");
}
images.forEach(ClubImage::unmarkThumbnail);
images.stream()
.filter(image -> image.id().equals(imageId))
.findFirst()
.ifPresent(ClubImage::markThumbnail);
}
이 규칙을 ClubImage 각각에 흩뿌리는 것이 아니라,
Club Aggregate Root가 통제하는 것이 DDD다운 설계다.
Application Service는 Presentation 영역과 Domain 영역을 연결하는 계층이다.
일종의 Facade 역할을 한다.
Application Service의 일반적인 흐름은 다음과 같다.
1. Repository에서 Aggregate를 조회한다.
2. Aggregate의 도메인 기능을 실행한다.
3. 변경된 Aggregate를 저장한다.
4. 결과를 반환한다.
Application Service는 도메인 로직을 직접 구현하면 안 된다.
나쁜 예시는 다음과 같다.
if (club.isDeleted()) {
throw new IllegalStateException("이미 삭제된 클럽입니다.");
}
club.setDeleted(true);
좋은 예시는 다음과 같다.
club.delete();
도메인 규칙은 Domain 영역에 있어야 한다.
Application Service는 그 도메인 기능을 호출하는 역할만 한다.
Application Service는 Presentation 영역에 의존하면 안 된다.
따라서 다음과 같은 객체를 Application Service에 직접 넘기는 것은 좋지 않다.
HttpServletRequest
HttpSession
HttpServletResponse
나쁜 예시는 다음과 같다.
public void createClub(HttpServletRequest request) {
Long memberId = Long.valueOf(request.getHeader("X-MEMBER-ID"));
}
좋은 방식은 Controller에서 필요한 값을 추출한 뒤 Command 객체로 전달하는 것이다.
public void createClub(CreateClubCommand command) {
...
}
Presentation 영역은 흔히 말하는 Controller 계층이다.
주요 책임은 다음과 같다.
HTTP 요청을 받는다.
Request DTO를 검증한다.
Application Service를 호출한다.
Response DTO로 변환한다.
HTTP 응답을 반환한다.
예시는 다음과 같다.
@RestController
@RequestMapping("/api/v1/clubs")
public class ClubController {
private final CreateClubService createClubService;
@PostMapping
public ResponseEntity<CreateClubResponse> createClub(
@Valid @RequestBody CreateClubRequest request,
Authentication authentication
) {
Long memberId = Long.valueOf(authentication.getName());
CreateClubResult result = createClubService.createClub(
new CreateClubCommand(
request.name(),
request.intro(),
memberId
)
);
return ResponseEntity.ok(
new CreateClubResponse(result.clubId(), result.name())
);
}
}
Controller는 도메인 규칙을 판단하지 않는다.
Controller는 요청과 응답의 변환에 집중한다.
Infrastructure 영역은 기술 세부사항을 담당한다.
예를 들면 다음과 같다.
JPA Entity
Spring Data JPA Repository
QueryDSL
Redis
S3
...
DDD 관점에서 Infrastructure는 Domain과 Application을 지원하는 구현 영역이다.
즉, Infrastructure가 중심이 아니라
Domain을 위해 존재하는 세부 구현체다.

무엇이던간에 첫번째 도전은 어렵다.
본격적인 코드 작성에 앞서서 어떤 구조인지 딥다이브해서 알아보았는데 역시나 쉽지는 않을것같다.
가장 어려울것같은 부분은
아마 한두가지가 아닐것같지만, 시작이 반이라는 말이 있듯이, 벌써 반이나 왔다는 생각을 가지고 나아가고자 한다 !
👏화이팅 나 자신👏