CQRS
CQRS란?
CQRSCommand Query Responsibility Segregation는 명령Command과 조회Query의 책임을 분리하는 소프트웨어 설계 패턴으로, 데이터를 변경하는 작업과 데이터를 조회하는 작업을 별도의 모델로 처리하는 것을 목표로 합니다. 이를 통해 시스템의 성능, 확장성, 복잡성을 관리할 수 있으며, 특히 대규모 시스템에서 유용하게 적용됩니다.
CQRS 예시: 도서 관리 시스템
다음은 도서 관리 시스템에 CQRS 패턴을 적용한 예시입니다.
명령 모델
명령Command 모델은 데이터를 변경하는 작업에 사용됩니다. 예를 들어, 도서 관리 시스템에서 새 책을 추가하는 명령 모델은 다음과 같습니다.
public class AddBookCommand
{
public string Title { get; set; }
public string Author { get; set; }
}
public class BookCommandHandler
{
private readonly IBookRepository _bookRepository;
public BookCommandHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public void Handle(AddBookCommand command)
{
var book = new Book { Title = command.Title, Author = command.Author };
_bookRepository.Add(book);
}
}
조회 모델
조회Query 모델은 데이터를 읽는 작업에 사용됩니다. 도서 목록을 조회하는 작업은 조회 모델로 처리됩니다.
public class BookQueryService
{
private readonly IBookReadRepository _bookReadRepository;
public BookQueryService(IBookReadRepository bookReadRepository)
{
_bookReadRepository = bookReadRepository;
}
public IEnumerable<BookDto> GetBooks()
{
return _bookReadRepository.GetAll();
}
}
위 예시에서 명령 모델은 AddBookCommand
를 처리하여 책을 추가하고, 조회 모델은 BookQueryService
를 통해 책 목록을 조회합니다. 이 두 모델은 서로 독립적으로 동작합니다.
장점
성능 최적화
CQRS를 통해 명령과 조회 작업을 분리하면, 각 작업에 맞는 최적화를 수행할 수 있습니다. 예를 들어, 조회 작업에서는 데이터베이스의 인덱스를 사용하거나 캐싱을 통해 빠른 성능을 제공할 수 있으며, 명령 작업에서는 데이터 일관성을 유지하며 안정적인 처리를 보장할 수 있습니다. 도서 관리 시스템에서는 조회 작업을 캐시하거나 인덱싱을 통해 속도를 높이는 반면, 명령 모델은 데이터 변경 시 일관성 있는 처리를 보장할 수 있습니다. 이렇게 각 작업을 독립적으로 최적화하면 시스템의 전체 성능이 개선됩니다.
확장성
CQRS는 명령과 조회를 별도의 시스템에서 처리할 수 있어 확장성이 뛰어납니다. 예를 들어, 조회 작업이 많은 시스템에서는 조회 모델을 독립적으로 확장하고, 데이터 변경이 빈번한 경우 명령 모델을 확장하는 방식으로 시스템을 확장할 수 있습니다. 도서 관리 시스템에서 다수의 사용자가 동시에 책 목록을 조회하는 상황이라면 조회 모델을 확장하여 성능을 보장하고, 반대로 새로운 책을 추가하는 명령 모델은 별도로 확장할 수 있습니다. 이는 전체 시스템의 확장성을 높이는 데 도움이 됩니다.
복잡성 관리
CQRS는 명령과 조회의 책임을 분리하여 코드의 복잡성을 줄입니다. 두 가지 책임을 독립적으로 설계할 수 있으므로, 코드가 명확해지고 유지보수가 쉬워집니다.
단점
복잡성 증가
CQRS는 두 가지 모델을 별도로 설계해야 하므로, 전체 시스템의 복잡성이 증가할 수 있습니다. 특히 작은 규모의 프로젝트에서는 이 패턴이 불필요하게 복잡할 수 있습니다.
데이터 일관성 문제
CQRS는 명령과 조회를 별도의 모델로 처리하기 때문에, 최종 일관성Eventual Consistency을 보장하는 경우가 많습니다. 이로 인해 조회 작업이 최신 데이터를 반영하지 않을 수 있으며, 실시간 데이터 일관성이 중요한 시스템에서는 문제가 될 수 있습니다. 도서 관리 시스템에서 책을 추가한 직후, 사용자가 책 목록을 조회했을 때 추가된 책이 즉시 보이지 않을 수 있습니다. 이러한 최종 일관성 문제는 사용자가 최신 데이터를 확인해야 하는 상황에서는 문제가 될 수 있습니다.
결론
CQRS는 대규모 시스템에서 성능, 확장성, 복잡성 문제를 해결하는 데 유용한 설계 패턴입니다. 하지만 작은 규모의 프로젝트에서는 불필요하게 복잡해질 수 있으며, 데이터 일관성 문제를 해결해야 할 수도 있습니다. 시스템의 요구 사항에 따라 CQRS 패턴을 적용하는 것이 중요합니다.
심화 학습
CQRS와 성능 최적화
CQRS는 읽기와 쓰기 작업을 분리하여 성능을 최적화하는 데 매우 유용합니다. 특히 조회 성능을 극대화하기 위해 CQRS에서는 읽기 저장소와 쓰기 저장소를 분리하여 각기 다른 방식으로 최적화할 수 있습니다.
읽기와 쓰기 저장소 분리
- 쓰기 저장소: 데이터 변경을 담당하는 저장소로, 주로 트랜잭션 처리와 일관성을 보장하는 데 중점을 둡니다.
- 읽기 저장소: 데이터 조회를 담당하는 저장소로, 성능을 극대화하기 위해 NoSQL 데이터베이스나 메모리 캐싱을 사용할 수 있습니다.
예시: 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 IBookWriteRepository _writeRepository;
private readonly IEventDispatcher _eventDispatcher;
public BorrowBookCommandHandler(IBookWriteRepository writeRepository, IEventDispatcher eventDispatcher)
{
_writeRepository = writeRepository;
_eventDispatcher = eventDispatcher;
}
public void Handle(BorrowBookCommand command)
{
var book = _writeRepository.FindById(command.BookId);
book.Borrow();
_writeRepository.Save(book);
var bookBorrowedEvent = new BookBorrowedEvent(book.Id, command.UserId, DateTime.UtcNow);
_eventDispatcher.Dispatch(bookBorrowedEvent);
}
}
// Query: 도서 상태 조회
public class BookQueryService
{
private readonly IBookReadRepository _readRepository;
public BookQueryService(IBookReadRepository readRepository)
=> _readRepository = readRepository;
public BookDto GetBookById(int bookId)
=> _readRepository.GetBookById(bookId);
}
- 쓰기 저장소(
IBookWriteRepository
)는 도서의 상태를 변경하는 작업을 담당합니다. - 읽기 저장소(
IBookReadRepository
)는 도서의 상태를 조회하는 작업을 처리합니다. - 각각의 저장소는 필요에 따라 다른 데이터베이스를 사용할 수 있으며, 읽기 저장소는 성능을 최적화하기 위해 별도로 구성할 수 있습니다.
CQRS에서의 일관성 관리
CQRS는 명령과 조회가 독립적으로 처리되기 때문에 최종 일관성Eventual Consistency 문제를 유발할 수 있습니다. 이는 시스템이 즉시 일관성을 보장하지 않으며, 일정 시간이 지난 후에 일관성을 유지하게 되는 것을 의미합니다. 최종 일관성 문제를 해결하기 위한 방법으로는 다음과 같은 전략을 사용할 수 있습니다.
이벤트 기반 일관성 관리
CQRS에서 최종 일관성을 유지하기 위해서는 명령과 조회 사이의 데이터를 동기화해야 합니다. 이를 위해 이벤트 기반 시스템을 사용하여, 상태 변경이 발생할 때마다 조회 저장소에 해당 변경 사항을 반영할 수 있습니다.
public class BookBorrowedEventHandler
{
private readonly IBookReadRepository _readRepository;
public BookBorrowedEventHandler(IBookReadRepository readRepository)
{
_readRepository = readRepository;
}
public void Handle(BookBorrowedEvent bookBorrowedEvent)
{
var book = _readRepository.GetBookById(bookBorrowedEvent.BookId);
book.IsBorrowed = true;
_readRepository.Save(book);
}
}
- 이벤트 핸들러는 도서 대출 이벤트가 발생할 때 조회 저장소에 해당 변경 사항을 반영하여, 조회 요청이 들어왔을 때 최신 상태를 제공할 수 있습니다.
CQRS와 마이크로서비스 아키텍처의 결합
CQRS는 마이크로서비스 아키텍처와 결합하여 각 서비스가 독립적으로 상태를 관리할 수 있도록 합니다. 각 마이크로서비스는 자신만의 명령 처리와 조회 처리를 담당하며, 도메인 이벤트를 통해 상호작용할 수 있습니다.
예시: 도서 대출 시스템에서 CQRS와 마이크로서비스
- 도서 서비스: 도서 관련 명령을 처리하는 마이크로서비스
- 회원 서비스: 사용자 관련 조회를 담당하는 마이크로서비스.
// 도서 서비스에서 도서 대출 처리
public class BorrowBookCommandHandler
{
private readonly IBookWriteRepository _writeRepository;
private readonly IEventDispatcher _eventDispatcher;
public void Handle(BorrowBookCommand command)
{
var book = _writeRepository.FindById(command.BookId);
book.Borrow();
_writeRepository.Save(book);
var bookBorrowedEvent = new BookBorrowedEvent(book.Id, command.UserId, DateTime.UtcNow);
_eventDispatcher.Dispatch(bookBorrowedEvent);
}
}
// 회원 서비스에서 회원 정보 조회
public class UserQueryService
{
private readonly IUserReadRepository _readRepository;
public UserDto GetUserById(int userId)
=> _readRepository.GetUserById(userId);
}
- 도서 서비스는 도서 대출 명령을 처리하고, 도메인 이벤트를 발행하여 상태 변화를 다른 서비스에 알립니다.
- 회원 서비스는 회원 정보를 조회하며, 각 서비스는 독립적으로 운영됩니다.