도메인 주도 개발(Domain-Driven Design, DDD)이란 무엇인가?
1. DDD의 정의
- 도메인 주도 개발(Domain-Driven Design, DDD)은 복잡한 비즈니스 도메인을 효과적으로 모델링하고 관리하기 위한 소프트웨어 개발 방법론입니다.
- DDD는 도메인 모델을 중심으로 애플리케이션을 설계하여 비즈니스 요구사항을 정확히 반영하고, 유지보수성을 높입니다.
2. DDD의 주요 개념
- 도메인(Domain): 소프트웨어가 해결하려는 비즈니스 영역이나 문제 공간.
- 유비쿼터스 언어(Ubiquitous Language): 팀 내 모든 구성원이 공통으로 사용하는 언어로, 도메인 모델과 소통을 일관되게 유지.
- 바운디드 컨텍스트(Bounded Context): 도메인을 여러 개의 독립된 컨텍스트로 분리하여 복잡성을 관리.
- 애그리거트(Aggregate): 도메인 모델 내에서 일관성을 유지해야 하는 객체들의 집합.
- 루트 애그리거트(Root Aggregate): 애그리거트의 진입점이 되는 핵심 엔티티.
- 벨류 오브젝트(Value Object): 식별자가 필요 없는 객체로, 속성만으로 식별됨.
- 엔티티(Entity): 고유한 식별자를 가지며, 지속적인 생명주기를 가진 도메인 객체.
왜 DDD를 사용하는가?
1. 코드 구조의 중요성
- 초기에는 단순한 구조로도 서비스 구현이 가능하지만, 애플리케이션이 커지고 복잡해질수록 코드의 유지보수성과 확장성이 저하됩니다.
- DDD는 서비스와 도메인 등의 명확한 구조를 통해 복잡성을 관리하고, 코드의 가독성과 유지보수성을 향상시킵니다.
2. 유지보수와 확장의 필요성
- 소프트웨어 개발은 초기 개발뿐만 아니라 지속적인 유지보수와 기능 개선이 포함됩니다.
- DDD는 도메인 로직을 명확히 분리하여 변경 사항을 안전하게 반영하고 유지보수를 용이하게 합니다.
3. 협업의 효율성
- 큰 규모의 서비스에서는 여러 개발자가 협업하게 됩니다.
- DDD는 공통된 도메인 모델과 바운디드 컨텍스트를 통해 팀 내 소통을 원활하게 합니다.
- 새로운 개발자가 팀에 합류했을 때, 도메인 모델과 구조를 쉽게 이해하고 요구사항을 파악할 수 있습니다.
4. 객체지향과 클린 코드의 중요성
- DDD는 객체지향 설계 원칙과 클린 코딩 방식을 강조하여, 코드의 재사용성과 유지보수성을 높입니다.
- 명확한 구조와 책임 분리를 통해 코드의 복잡성을 줄이고, 이해하기 쉬운 코드를 작성할 수 있습니다.
5. 대규모 서비스에의 적합성
- DDD는 특히 대규모 서비스에 적합한 방법론으로, 복잡한 도메인을 효과적으로 관리할 수 있습니다.
- 규모가 커질수록 DDD의 장점이 더욱 두드러지며, 여러 개발자가 동시에 작업할 때의 협업 효율성을 높여줍니다.
DDD가 아니다! 단순 엔티티 중심 구조의 한계
1. 엔티티 중심 구조의 예
- 예를 들어,
Notice
라는 엔티티를 중심으로 패키지를 구성:
- 문제점:
- 단순히 엔티티를 기준으로 패키지를 나누는 것만으로는 도메인의 복잡성을 관리하기 어렵습니다.
- 비즈니스 로직이 서비스나 컨트롤러에 흩어져 있어 도메인 모델의 일관성이 떨어집니다.
- 새로운 개발자가 합류했을 때, 도메인 모델을 이해하기 어려워집니다.
2. 왜 이는 DDD가 아닌가?
- 진정한 DDD의 요소 부족:
- 루트 애그리거트(Root Aggregate): 단순한 엔티티 구조는 애그리거트의 개념을 반영하지 못합니다.
- 벨류 오브젝트(Value Object): 속성만을 가지는 객체의 명확한 정의와 사용이 없습니다.
- 유비쿼터스 언어(Ubiquitous Language): 팀 내에서 일관된 도메인 언어의 사용이 부족합니다.
- 도메인 서비스(Domain Service): 비즈니스 로직을 도메인 서비스로 분리하지 않습니다.
- 루트 애그리거트의 불변성: 루트 애그리거트는 불변성을 유지하며, 하위 도메인은 루트 애그리거트를 통해서만 접근할 수 있습니다.
진정한 DDD로의 전환: Notice 예시
1. 도메인 모델링
- Notice 도메인:
Notice
는 도메인의 루트 애그리거트로 설정됩니다.
Notice
는 여러 Comment
를 가질 수 있으며, Comment
는 Value Object
로 정의됩니다.
Notice
는 비즈니스 로직을 포함하여 도메인 모델의 일관성을 유지합니다.
- 루트 애그리거트의 불변성: 루트 애그리거트는 불변성을 유지하며, 상태 변경이 필요할 경우 새로운 값을 할당합니다.
2. 주요 구성 요소 설명
2.1. 루트 애그리거트(Root Aggregate)
Notice.java
public class Notice {
private NoticeId id;
private String title;
private String content;
private List<Comment> comments;
public Notice(NoticeId id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
this.comments = new ArrayList<>();
}
public void addComment(Comment comment) {
// 비즈니스 로직 포함
this.comments.add(comment);
}
// 기타 비즈니스 메서드
}
- 설명:
Notice
는 루트 애그리거트로, Notice
를 통해서만 Comment
에 접근할 수 있습니다.
- 루트 애그리거트는 불변성을 유지하며, 상태 변경 시 새로운 인스턴스를 생성해야 합니다.
- 루트 애그리거트는 다른 애그리거트와의 직접적인 의존성을 피하고, 필요한 경우 도메인 서비스를 통해 상호 작용합니다.
2.2. 벨류 오브젝트(Value Object)
public class Comment {
private final String author;
private final String message;
public Comment(String author, String message) {
this.author = author;
this.message = message;
}
// 불변성을 유지하기 위한 메서드들
}
- 설명:
Comment
는 식별자가 필요 없는 벨류 오브젝트로, Notice
에 종속적으로 존재합니다.
- 벨류 오브젝트는 불변성을 유지하여, 상태 변경 시 새로운 인스턴스를 생성합니다.
2.3. 유비쿼터스 언어(Ubiquitous Language)
- 팀 내 모든 구성원이
Notice
, Comment
, NoticeId
등 도메인 용어를 일관되게 사용하여 소통합니다.
- 예시:
- "공지", "댓글", "공지 ID" 등의 용어를 통일하여 사용함으로써 오해를 줄이고 도메인 모델을 명확히 이해할 수 있습니다.
2.4. 도메인 서비스(Domain Service)
public class NoticeDomainService {
public void publishNotice(Notice notice) {
// 공지 게시 로직
}
}
- 설명:
- 도메인 서비스는 특정 엔티티나 값 객체에 속하지 않는 비즈니스 로직을 담당합니다.
- 예를 들어, 공지 게시와 같은 로직을 도메인 서비스에서 처리하여 도메인 모델의 책임을 명확히 합니다.
- 루트 애그리거트와의 관계: 도메인 서비스는 루트 애그리거트를 통해 도메인 로직을 수행합니다.
2.5. 애플리케이션 서비스(Application Service)
public class NoticeApplicationService {
private final NoticeRepository noticeRepository;
private final NoticeDomainService noticeDomainService;
public NoticeApplicationService(NoticeRepository noticeRepository, NoticeDomainService noticeDomainService) {
this.noticeRepository = noticeRepository;
this.noticeDomainService = noticeDomainService;
}
public void createNotice(NoticeDto noticeDto) {
Notice notice = new Notice(noticeDto.getId(), noticeDto.getTitle(), noticeDto.getContent());
noticeRepository.save(notice);
}
public void publishNotice(Long noticeId) {
Notice notice = noticeRepository.findById(noticeId);
noticeDomainService.publishNotice(notice);
noticeRepository.save(notice);
}
}
- 설명:
- 애플리케이션 서비스는 도메인 모델을 활용하여 비즈니스 작업을 수행합니다.
- 도메인 서비스나 도메인 모델 자체의 메서드를 호출하여 비즈니스 로직을 처리합니다.
- 책임 분리: 애플리케이션 서비스는 인프라스트럭처와 도메인 로직을 연결하는 역할을 담당합니다.
도메인과 데이터 모델의 분리
1. 도메인 모델(Domain Model)
- 설명:
- 비즈니스 로직과 규칙을 반영한 도메인 모델을 의미합니다.
- 도메인 모델은 도메인 전문가와 개발자가 협력하여 설계한 비즈니스 개념을 구현합니다.
- 특징:
- 비즈니스 요구사항을 충족시키기 위해 설계된 객체들로 구성됩니다.
- 도메인 모델은 비즈니스 로직을 포함하여 도메인의 일관성을 유지합니다.
2. 데이터 모델(Data Model) / 인프라스트럭처 모델(Infrastructure Model)
- 설명:
- 데이터베이스의 스키마나 테이블 구조를 의미합니다.
- 주로 데이터 저장과 관련된 구조로, 비즈니스 로직과는 분리되어 있습니다.
- 특징:
- 데이터베이스 중심의 설계
- 저장, 조회, 업데이트 등의 데이터 조작에 초점
- ORM(Object-Relational Mapping)을 사용하여 도메인 모델과 매핑할 수 있습니다.
3. 도메인 모델과 데이터 모델의 차이
- 도메인 모델:
- 비즈니스 로직과 규칙을 중심으로 설계됨.
- 도메인의 일관성을 유지하고, 비즈니스 요구사항을 충족.
- 데이터 모델:
- 데이터 저장과 접근을 중심으로 설계됨.
- 데이터베이스의 스키마와 일치하도록 구성됨.
- 분리의 중요성:
- 도메인 모델과 데이터 모델을 분리함으로써, 도메인 로직의 변경이 데이터 저장 구조에 영향을 미치지 않도록 합니다.
- 유지보수성과 확장성을 높이고, 비즈니스 로직과 데이터 저장 로직의 책임을 명확히 구분할 수 있습니다.
4. 애그리거트와 데이터 모델의 관계
- 애그리거트(Aggregate):
- 도메인 모델의 일관성을 유지하는 단위.
- 루트 애그리거트를 통해 접근하며, 다른 애그리거트와의 직접적인 의존성을 피함.
- 데이터 모델:
- 데이터베이스 스키마는 애그리거트의 구조와 다를 수 있음.
- ORM(Object-Relational Mapping)을 사용하여 도메인 모델과 데이터 모델을 매핑.
루트 애그리거트의 특성과 규칙
1. 루트 애그리거트의 역할
- 루트 애그리거트(Root Aggregate)는 애그리거트의 진입점으로, 다른 애그리거트나 하위 도메인에 대한 접근을 제어합니다.
- 루트 애그리거트는 불변성(Immutability)을 유지하며, 상태 변경이 필요할 경우 새로운 인스턴스를 할당합니다.
2. 루트 애그리거트의 규칙
- 하위 도메인 접근: 하위 도메인은 반드시 루트 애그리거트를 통해서만 접근할 수 있습니다.
- 불변성 유지: 루트 애그리거트는 불변성을 유지하여, 상태 변경 시 새로운 값을 할당해야 합니다.
- 루트 애그리거트 간의 관계: 루트 애그리거트는 서로 직접적으로 참조하지 않으며, 필요한 경우 도메인 서비스를 통해 간접적으로 상호 작용합니다.
3. 예시: Notice와 User 애그리거트
- 시나리오:
Notice
와 User
는 각각 별도의 루트 애그리거트입니다.
Notice
에서 User
정보를 필요로 할 때, Notice
패키지 내에 새로운 하위 도메인으로 User
를 생성합니다.
- 설명:
Notice
와 User
는 독립적인 루트 애그리거트로 관리됩니다.
Notice
패키지 내에 User
관련 로직을 포함하지 않으며, 필요 시 도메인 서비스를 통해 User
와 상호 작용합니다.
루트 애그리거트와 엔티티
1. 도메인과 엔티티의 차이
- 도메인(Domain):
- 비즈니스 영역이나 문제 공간을 의미하며, 도메인의 개념은 매우 포괄적입니다.
- 도메인 모델은 도메인 내의 모든 개념, 규칙, 로직을 포함합니다.
- 엔티티(Entity):
- 도메인 모델 내에서 고유한 식별자를 가지며, 지속적인 생명주기를 가진 객체입니다.
- 엔티티는 도메인 모델의 중요한 개념을 구현하며, 그 자체로 비즈니스 로직을 포함할 수 있습니다.
2. 루트 애그리거트와 @Entity 어노테이션
- 루트 애그리거트는 반드시 @Entity 어노테이션을 사용해야 하는 것은 아닙니다.
- 설명:
- 루트 애그리거트는 도메인 모델의 핵심 개념을 표현하며, 반드시 데이터베이스 엔티티와 일치하지 않을 수 있습니다.
- ORM을 사용할 경우, 루트 애그리거트는
@Entity
어노테이션을 사용하여 데이터베이스와 매핑할 수 있지만, 이는 선택 사항입니다.
- 데이터 모델과의 분리:
- 루트 애그리거트는 도메인 로직에 집중하고, 데이터 모델은 인프라스트럭처 계층에서 관리하여 도메인과 데이터 저장을 분리합니다.
진정한 DDD의 구현을 통한 장점
1. 도메인 로직의 일관성 유지
- 모든 비즈니스 로직이 도메인 모델과 도메인 서비스에 집중되어 있습니다.
- 도메인 모델의 책임이 명확히 정의되어 코드의 일관성을 유지할 수 있습니다.
2. 유비쿼터스 언어의 적용
- 팀 내 소통이 원활해지고, 도메인 이해도가 높아집니다.
- 공통된 언어를 사용하여 오해를 줄이고, 도메인 모델을 정확히 반영합니다.
3. 복잡성 관리
- 바운디드 컨텍스트와 애그리거트를 통해 도메인의 복잡성을 효과적으로 관리할 수 있습니다.
- 각 컨텍스트가 독립적으로 관리되므로, 시스템의 확장성과 유지보수성이 향상됩니다.
4. 협업 효율성 증대
- 명확한 도메인 모델과 구조 덕분에 팀 내 협업이 용이해집니다.
- 새로운 개발자가 팀에 합류했을 때, 도메인 모델과 구조를 쉽게 이해하고 요구사항을 파악할 수 있습니다.
결론
- 도메인 주도 개발(Domain-Driven Design, DDD)은 단순히 엔티티 중심의 패키지 구조가 아닌, 도메인 모델의 깊이 있는 설계와 팀 간의 협업을 강조하는 방법론입니다.
- DDD를 올바르게 적용하면, 복잡한 비즈니스 로직을 효과적으로 관리하고, 유지보수성과 확장성을 크게 향상시킬 수 있습니다.
- 프로젝트의 규모와 복잡성에 따라 DDD의 적용 여부를 신중히 고려하고, 진정한 DDD의 개념을 이해한 후 적용하는 것이 중요합니다.