도메인 주도 설계
도메인 주도 설계란?
도메인 주도 설계Domain-Driven Design, DDD는 복잡한 비즈니스 문제를 해결하기 위해 비즈니스 도메인에 맞춰 소프트웨어 시스템을 설계하는 방법론입니다. DDD는 도메인 전문가와 개발자가 함께 문제를 정의하고, 이를 해결하는 구조를 개발하는 데 초점을 맞춥니다. 이를 위해 도메인 모델을 기반으로 시스템을 설계하며, 도메인의 복잡한 비즈니스 규칙과 흐름을 소프트웨어에 반영할 수 있는 다양한 개념을 사용합니다.
DDD의 주요 개념
DDD는 비즈니스 문제를 해결하기 위한 여러 핵심 개념을 정의합니다. 이 개념들은 도메인 모델을 통해 시스템의 요구 사항을 정확히 반영하는 데 중요한 역할을 합니다.
도메인
도메인Domain은 소프트웨어가 해결하려는 비즈니스 문제의 범위를 정의하는 개념입니다. 예를 들어, 도서관 관리 시스템에서는 도서 대출, 반납, 사용자 관리가 도메인에 해당합니다. 도메인은 시스템이 해결하고자 하는 비즈니스 문제의 중심이며, 도메인의 규칙과 흐름이 시스템 설계에 중요한 역할을 합니다.
서브도메인
도메인은 매우 복잡할 수 있기 때문에 여러 서브도메인Subdomain으로 나눌 수 있습니다. 서브도메인은 도메인의 특정 부분에 집중하며, 비즈니스에서 각각의 역할을 수행합니다. 예를 들어, 도서관 시스템의 서브도메인에는 대출 관리 서브도메인과 회원 관리 서브도메인이 있을 수 있습니다.
바운디드 컨텍스트
바운디드 컨텍스트Bounded Context는 하나의 도메인 모델 내에서 특정 용어와 규칙이 일관되게 적용되는 경계를 정의합니다. 바운디드 컨텍스트는 시스템 내에서 같은 용어가 다른 의미를 가지는 것을 방지하고, 도메인 모델을 각 서브도메인에 맞게 분리하여 관리할 수 있습니다. 예시:
- 대출 관리 컨텍스트: “사용자"는 도서 대출 권한을 가진 사람.
- 회원 관리 컨텍스트: “사용자"는 회원 자격을 가진 사람.
엔터티
엔터티Entity는 고유한 식별자를 가진 객체로, 상태가 변할 수 있는 중요한 비즈니스 개체를 의미합니다. 도메인 내에서 엔터티는 비즈니스 규칙에 따라 상태가 변경될 수 있습니다.
public class Book
{
public int Id { get; private set; }
public string Title { get; private set; }
public bool IsBorrowed { get; private set; }
public Book(int id, string title)
{
Id = id;
Title = title;
IsBorrowed = false;
}
public void Borrow()
{
if (IsBorrowed)
throw new InvalidOperationException("Already borrowed");
IsBorrowed = true;
}
public void Return()
{
IsBorrowed = false;
}
}
Book
엔터티는 고유 식별자(Id
)를 가지며, 도서의 대출 상태(IsBorrowed
)를 관리합니다.- 대출(
Borrow
)과 반납(Return
) 같은 상태 변화를 처리하는 메서드들이 포함되어 있습니다.
값 객체
값 객체Value Object는 식별자를 가지지 않으며, 속성 값으로만 동일성을 판단합니다. 값 객체는 주로 불변성immutable을 가지며, 상태가 변경되지 않습니다.
public class Address
{
public string Street { get; }
public string City { get; }
public Address(string street, string city)
{
Street = street;
City = city;
}
}
Address
클래스는 불변성을 가진 값 객체로, 주소 정보를 나타냅니다.- 고유 식별자가 없고 속성 값이 같으면 동일한 객체로 간주됩니다.
애그리게이트와 애그리게이트 루트
애그리게이트Aggregate는 서로 관련된 여러 엔터티나 값 객체의 집합을 의미하며, 애그리게이트 루트Aggregate Root는 애그리게이트 내부 상태에 접근할 수 있는 유일한 진입점입니다. 외부에서 애그리게이트 내부 상태를 변경할 수 있는 방법은 오직 애그리게이트 루트를 통해서만 가능합니다.
public class BookBorrowing
{
public Book Book { get; private set; }
public User Borrower { get; private set; }
public BookBorrowing(Book book, User borrower)
{
Book = book;
Borrower = borrower;
}
public void ReturnBook()
{
Book.Return();
}
}
BookBorrowing
은Book
과User
를 포함하는 애그리게이트입니다.- 도서 대출 및 반납 로직은 애그리게이트 루트를 통해 일관되게 처리됩니다.
리포지토리
리포지토리Repository는 엔터티의 영속성과 조회를 담당합니다. 도메인 모델이 데이터베이스와 상호작용하는 방식을 추상화하여, 비즈니스 로직이 데이터베이스와의 세부사항에 의존하지 않도록 합니다.
public interface IBookRepository
{
Book FindById(int id);
void Save(Book book);
}
public class BookRepository : IBookRepository
{
private readonly List<Book> _books = new();
public Book FindById(int id) => _books.FirstOrDefault(b => b.Id == id);
public void Save(Book book) => _books.Add(book);
}
IBookRepository
는Book
데이터를 저장하고 조회하는 작업을 추상화한 리포지토리 인터페이스입니다.- 이를 통해 비즈니스 로직이 데이터베이스에 직접 의존하지 않게 됩니다.
DDD의 방법론
DDD 방법론은 크게 전략적 설계와 전술적 설계로 나뉩니다.
전략적 설계
전략적 설계는 시스템의 전반적인 구조를 정의하고, 각 도메인과 서브도메인의 관계를 명확히 하는 데 중점을 둡니다. 이 단계에서는 바운디드 컨텍스트와 도메인 간의 관계를 정의하고, 각 도메인 모델의 경계를 설정합니다.
바운디드 컨텍스트
바운디드 컨텍스트는 각 도메인이 일관된 모델과 규칙을 사용하는 경계를 정의하는 역할을 합니다. 이는 복잡한 도메인 모델을 작은 단위로 나누어 관리하고, 각 컨텍스트 내에서 모델의 의미를 명확히 할 수 있도록 돕습니다.
도메인 간 통신
바운디드 컨텍스트 간에는 독립성을 유지하면서도 필요한 경우 통신이 이루어져야 합니다. 도메인 이벤트는 이를 위해 사용될 수 있는 강력한 도구로, 이벤트 기반으로 도메인 간 느슨한 결합을 유지하며 통신을 처리할 수 있습니다.
public class BookBorrowedEvent
{
public int BookId { get; }
public string UserId { get; }
public BookBorrowedEvent(int bookId, string userId)
{
BookId = bookId;
UserId = userId;
}
}
BookBorrowedEvent
는 책이 대출되었을 때 발생하는 도메인 이벤트로, 다른 바운디드 컨텍스트나 시스템이 이 이벤트에 반응할 수 있도록 합니다.
전술적 설계
전술적 설계는 도메인 모델의 세부 구현을 다룹니다. 엔터티, 값 객체, 리포지토리와 같은 요소들이 전술적 설계의 핵심 요소이며, 이를 통해 비즈니스 로직을 코드로 구체화합니다.
엔터티와 값 객체
전술적 설계에서는 엔터티와 값 객체를 통해 비즈니스 규칙을 표현하고, 상태 변경을 관리합니다. 엔터티는 고유한 식별자를 통해 구분되고, 값 객체는 불변성을 유지합니다.
리포지토리
리포지토리는 엔터티의 영속성을 관리하며, 데이터 저장소와 도메인 모델 간의 상호작용을 추상화합니다. 이를 통해 도메인 모델이 데이터베이스에 의존하지 않도록 하고, 비즈니스 로직이 데이터 저장소와 분리되도록 설계합니다.
맺음말
도메인 주도 설계는 복잡한 비즈니스 문제를 해결하는 강력한 방법론으로, 도메인의 본질을 소프트웨어에 반영하여 시스템이 비즈니스 요구사항에 맞춰 유연하게 동작할 수 있도록 합니다. 전략적 설계는 도메인 간의 관계와 경계를 명확히 하여 전체 시스템의 구조를 정의하고, 전술적 설계는 도메인 모델의 구체적인 구현을 통해 비즈니스 로직을 처리합니다. 이를 통해 비즈니스 요구사항 변화에도 유연하게 대처할 수 있는 확장 가능하고 유지보수하기 쉬운 시스템을 구축할 수 있습니다.
심화 학습
애그리게이트 설계 최적화
애그리게이트는 도메인의 중요한 비즈니스 로직을 캡슐화하고, 일관성을 유지하는 집합체입니다. 하지만 모든 애그리게이트가 동일한 크기나 복잡성을 가질 필요는 없으며, 성능 및 유지보수성의 균형을 맞추는 것이 중요합니다.
애그리게이트 설계에서의 고려사항
- 작은 애그리게이트: 작은 애그리게이트는 트랜잭션 처리 비용을 줄이고 성능을 최적화할 수 있습니다. 각 애그리게이트는 명확한 경계를 가져야 하며, 외부 애그리게이트와 상호작용할 때는 직접 참조보다는 식별자를 통해 간접적으로 참조하는 것이 좋습니다.
예시: 작은 애그리게이트의 설계
public class Book
{
public int Id { get; private set; }
public string Title { get; private set; }
public bool IsBorrowed { get; private set; }
public void Borrow()
{
if (IsBorrowed)
throw new InvalidOperationException("Already borrowed");
IsBorrowed = true;
}
public void Return()
{
IsBorrowed = false;
}
}
Book
애그리게이트는 단순한 대출과 반납 로직을 처리하며, 트랜잭션이 가벼워집니다.
도메인 이벤트를 활용한 설계 확장
도메인 이벤트는 시스템 내의 중요한 상태 변화Event를 다른 부분에 전달하는 방식입니다. 도메인 이벤트를 통해 모듈 간의 결합도를 낮추고, 시스템을 확장 가능한 상태로 유지할 수 있습니다.
도메인 이벤트의 활용
- 상태 변화 알림: 애그리게이트의 상태가 변경될 때, 도메인 이벤트를 발행하여 다른 시스템 모듈들이 이를 감지하고 적절히 대응할 수 있습니다.
- 확장성: 도메인 이벤트를 통해 모듈 간의 결합도를 줄이고, 시스템의 확장성을 높일 수 있습니다.
예시: 도메인 이벤트
public class BookBorrowedEvent
{
public int BookId { get; }
public int UserId { get; }
public DateTime BorrowedAt { get; }
public BookBorrowedEvent(int bookId, int userId, DateTime borrowedAt)
{
BookId = bookId;
UserId = userId;
BorrowedAt = borrowedAt;
}
}
public class BorrowService
{
private readonly IEventDispatcher _eventDispatcher;
public BorrowService(IEventDispatcher eventDispatcher)
{
_eventDispatcher = eventDispatcher;
}
public void BorrowBook(Book book, User user)
{
book.Borrow();
var bookBorrowedEvent = new BookBorrowedEvent(book.Id, user.Id, DateTime.Now);
_eventDispatcher.Dispatch(bookBorrowedEvent);
}
}
BookBorrowedEvent
는 책이 대출될 때 발생하는 이벤트로, 다른 시스템 모듈이 이를 감지하여 후속 작업을 처리할 수 있게 합니다.
CQRS와 이벤트 소싱을 활용한 DDD 구현
CQRSCommand Query Responsibility Segregation와 이벤트 소싱Event Sourcing은 복잡한 도메인 로직을 확장하는 데 유용한 패턴입니다. 특히 DDD에서 복잡한 상태 변경을 효율적으로 처리하기 위해 CQRS와 이벤트 소싱을 결합할 수 있습니다.
CQRS: 쓰기와 읽기의 분리
CQRS 패턴에서는 쓰기 작업Command과 읽기 작업Query을 분리하여, 복잡한 비즈니스 로직을 더욱 효율적으로 관리합니다.
이벤트 소싱: 상태 변경 추적
이벤트 소싱은 시스템의 상태를 이벤트로 기록하고, 이러한 이벤트의 집합을 통해 현재 상태를 유추하는 방식입니다. 이 방식은 시스템이 과거 상태를 재구성할 수 있도록 하며, 모든 상태 변화의 히스토리를 보존할 수 있습니다.
예시: CQRS와 이벤트 소싱 적용
// Command: 도서 대출
public class BorrowBookCommand
{
public int BookId { get; }
public int UserId { get; }
public BorrowBookCommand(int bookId, int userId)
{
BookId = bookId;
UserId = userId;
}
}
// Command Handler: 대출 처리
public class BorrowBookCommandHandler
{
private readonly IBookRepository _bookRepository;
public BorrowBookCommandHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public void Handle(BorrowBookCommand command)
{
var book = _bookRepository.FindById(command.BookId);
book.Borrow();
_bookRepository.Save(book);
}
}
// Event Sourcing: 이벤트로 상태 추적
public class EventSourcedBook
{
public int Id { get; private set; }
public bool IsBorrowed { get; private set; }
public void Apply(BookBorrowedEvent evt) => IsBorrowed = true;
public void Apply(BookReturnedEvent evt) => IsBorrowed = false;
}
BorrowBookCommand
는 대출을 처리하는 쓰기 작업을 담당하고, 이벤트 소싱 방식으로 상태 변경을 추적합니다.- 시스템은 과거의 이벤트를 모두 모아 책의 현재 상태를 계산할 수 있습니다.
컨텍스트 매핑을 통한 도메인 경계 관리
DDD에서 바운디드 컨텍스트는 도메인 모델의 경계를 정의하는 중요한 개념입니다. 하지만 대규모 시스템에서는 여러 바운디드 컨텍스트 간의 상호작용을 명확하게 관리해야 합니다. 이를 위해 컨텍스트 매핑을 사용합니다.
컨텍스트 매핑의 종류
- 공유 커널Shared Kernel: 두 개 이상의 바운디드 컨텍스트가 공통으로 사용하는 도메인 모델을 공유하는 경우.
- 반복자Anticorruption Layer: 한 컨텍스트의 모델을 다른 컨텍스트에서 사용할 때, 그 차이를 완충하는 레이어를 사용하는 경우.
예시: 컨텍스트 매핑
- 도서 대출 컨텍스트: 대출과 관련된 도메인 모델을 관리.
- 회원 관리 컨텍스트: 사용자 정보와 회원 자격을 관리.
- 반복자 패턴을 사용하여 두 컨텍스트 간 모델 차이를 완충.
컨텍스트 매핑을 통해 각 바운디드 컨텍스트는 독립적으로 유지되지만, 상호작용할 때 필요한 연결을 효과적으로 관리할 수 있습니다.
도메인 주도 설계에서의 성능 고려
DDD는 비즈니스 복잡성을 모델링하는 데 중점을 두기 때문에, 성능 문제를 고려하는 것도 중요합니다. 애그리게이트 크기, 트랜잭션 경계, 데이터베이스 설계 등을 신중하게 고려해야 합니다.
캐싱과 데이터베이스 분할
애그리게이트가 커지거나 트랜잭션 경계가 넓어질수록 성능 문제가 발생할 수 있습니다. 이를 해결하기 위해 캐싱을 사용하거나 데이터베이스를 분할하여 성능을 개선할 수 있습니다.
예시: 캐싱 적용
public class CachedBookRepository : IBookRepository
{
private readonly IBookRepository _inner;
private readonly IMemoryCache _cache;
public CachedBookRepository(IBookRepository inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public Book FindById(int id)
{
return _cache.GetOrCreate(id, entry => _inner.FindById(id));
}
public void Save(Book book)
{
_inner.Save(book);
_cache.Remove(book.Id); // 데이터가 변경되면 캐시 제거
}
}
CachedBookRepository
는 데이터베이스 접근을 최적화하기 위해 캐싱을 사용하여 성능을 개선합니다.- 성능 저하를 방지하기 위해, 트랜잭션 처리에 신경 써야 하며, 캐싱된 데이터를 적절히 관리하는 것이 중요합니다.
DDD와 마이크로서비스의 통합
DDD는 마이크로서비스 아키텍처와 결합하여 대규모 시스템의 복잡성을 관리할 수 있습니다. 각 바운디드 컨텍스트를 독립적인 마이크로 서비스로 분리함으로써, 시스템 간의 결합도를 줄이고 확장성을 높일 수 있습니다.
예시: 도서 대출 시스템에서의 마이크로서비스 적용
- 도서 대출 서비스: 도서 대출 관련 기능을 담당하는 마이크로서비스.
- 회원 관리 서비스: 사용자와 회원 자격을 관리하는 마이크로서비스.
- 두 서비스는 이벤트를 통해 통신하며, 도메인 이벤트를 발행하여 상호작용을 처리.
// 도서 대출 서비스에서 도메인 이벤트 발행
public class BorrowService
{
private readonly IEventDispatcher _eventDispatcher;
public void BorrowBook(Book book, User user)
{
book.Borrow();
var bookBorrowedEvent = new BookBorrowedEvent(book.Id, user.Id, DateTime.Now);
_eventDispatcher.Dispatch(bookBorrowedEvent);
}
}
- 도서 대출 서비스는 도메인 이벤트를 통해 상태 변화를 회원 관리 서비스로 전달할 수 있습니다.
- 각 서비스는 독립적으로 운영되며, 이벤트 기반으로 상호작용합니다.