CQRS
CQRS는 Command and Query Responsibility Segregation의 약자로, 데이터 저장소로부터 데이터의 조회(R, Read) 작업과 명령(CUD, Create/Update/Delete) 작업을 분리하는 패턴을 말한다.
전통적인 CRUD 구조의 애플리케이션에서 개발 및 운영을 하다보면, Domain Model의 복잡도가 증가하여 유지보수 비용이 증가하고 설계의 방향과 다르게 변질되기도 한다. 읽기와 쓰기의 데이터 표현이 서로 일치하지 않는 경우가 있고, 동일한 데이터에 대해 병렬로 작업이 수행되면 데이터 경합이 발생하기도 한다.
이를 해결하기 위해 명령에 대한 책임과 조회에 대한 책임을 분리하는 패턴이 CQRS 패턴이다. 읽기와 쓰기를 각각 다른 모델로 분리하여, 명령(Command)를 통해 데이터를 쓰고 쿼리(Query)를 통해 데이터를 읽는다.
CQRS 구조
이와 같은 계층 구조를 가지고 있는 애플리케이션에 CQRS 패턴을 적용하는 방법은 읽기/쓰기의 모델만 분리하는 방법과 물리적으로 분리하는 방법이 있다.
1.
읽기/쓰기의 모델 분리
단일 DB에 Command Model과 Query Model을 분리하는 방식이다.
DB는 분리하지 않기 때문에 간단하게 코드를 통해서 구현 및 적용이 가능하다.
하지만, 조회와 명령에 동일한 DB를 사용하여 발생하는 성능 이슈는 개선할 수 없다.
2.
읽기/쓰기의 물리적 분리
명령 DB와 조회 DB를 분리하고, Amazon의 람다와 같은 Broker나 별도의 동기화 방법을 통해 두 DB의 데이터 동기화를 처리하는 방식이다.
DB를 분리하여 사용함으로써 성능 문제를 해결할 수 있고, polyglot 구조로 조회 로직에 맞춰 알맞는 DB를 선택하거나 Model에 맞춰 튜닝할 수 있다. 일반적으로 읽기 저장소는 최적화된 역정규화 모델을 사용하여, 조회 성능을 극대화한다.
하지만, 애플리케이션과 모델에 맞는 별도의 DB를 구축해야하고 ORM 툴을 통해 DB 스키마로 자동 생성할 수 없다. 또한 Broker나 별도의 동기화 처리에 의존하게 되어 동기화에 대한 가용성과 신뢰도가 보장되어야한다는 단점이 있다. 만약 이벤트 소싱을 적용한다면 DB 업데이트와 이벤트 발행은 반드시 트랜잭션 안에서 이루어져야 한다.
polyglot
다수의 DB를 혼용해서 사용하는 구조
CQRS의 장단점
CQRS를 적용하면 여러 이점들을 얻을 수 있다.
•
조회와 명령의 워크로드를 독립적으로 확장하거나 크기를 조정할 수 있다. 이를 통해 Lock 경쟁이 더 적게 발생하도록 만들 수 있다.
•
조회에는 읽기에 최적화된 스키마를 사용하고, 명령에는 쓰기와 수정에 최적화된 스키마를 사용하여 성능을 극대화 할 수 있다.
•
읽기와 쓰기를 분리함으로써 보안 관리가 용이해진다.
•
쓰기와 읽기의 관심사에 따라 Model을 분리하여, 유연하고 유지보수하기 용이하게 작성할 수 있다.
•
RDBMS에서 읽기를 수행하지 않아 복잡한 Join 연산을 방지할 수 있다.
CQRS를 적용할 때 고려해야하는 단점들도 있다.
•
이벤트 소싱 패턴을 포함하여 구현하게되는 경우 복잡한 구조를 가지게될 수 있다.
•
메세징이 CQRS의 필수요소는 아니지만, 명령을 수행하고 업데이트 이벤트를 발행하는데 보편적으로 사용된다. 메세징을 통해 이벤트 실패나 중복 이벤트 발행에 대한 처리를 확실하게 해야한다.
•
읽기와 쓰기 저장소를 분리하면, 각각의 저장소에 있는 데이터의 동기화를 유지하여 데이터 일관성을 지켜야한다. 하지만 그럼에도 즉시적인 데이터 일관성이 보장되지는 않는다.
CQRS 패턴이 필요한 시점
CQRS는 단순한 개념이지만, 처리할 때 명령 처리기 패턴(Command Processor Pattern)이나 다계층 아키텍처(Multi-tier Architetcure), 이벤트 소싱(Event Sourcing)을 다루어 처리한다. 또는 DDD(Domain-Driven Desing)을 조합하기도 한다.
또한 개념적으로 어렵거나 동시성 등 기술적인 문제들로 인해 모든 연산이 명령과 조회로 쉽게 양분되지는 않는다. 이런 점들을 인지하고 다음과 같은 경우에 CQRS 패턴의 사용을 고려해볼 수 있다.
•
UX와 비즈니스 요구사항이 복잡해질 때
•
많은 사용자가 동일한 데이터에 동시에 액세스하여 경쟁 상태가 자주 발생할 때
•
데이터 읽기의 성능이 쓰기의 성능과 별개로 조정이 필요하거나 책임을 분리해야할 때
•
읽기에 비해 조회가 많이 발생하여 조회 성능을 보다 높이고 싶을 때
•
시스템 확장성을 높이고 싶을 때
이벤트 소싱과 CQRS 패턴
CQRS 패턴은 이벤트 소싱 패턴과 자주 함께 사용된다. 이벤트 소싱 패턴과 CQRS 패턴을 같이 사용하는 경우에는 이벤트 저장소가 쓰기 모델이 되고, 이벤트 저장소가 메인 저장소가 되어 읽기 저장소와 쓰기 저장소에 데이터를 저장한다.
이처럼 이벤트 소싱은 특정 시점의 실제 데이터를 이벤트 스트림을 쓰기 저장소로써 사용하여, 하나의 aggregate에 대한 병합 충돌을 방지하고 성능과 확장성을 극대화 시킨다. 이벤트들은 비동기적으로 읽기 저장소의 역정규화 모델을 생성하고 변경하는데 사용된다.
이벤트 소싱 패턴을 적용하면 시스템이 변경되거나 읽기 모델이 바뀌는 경우, 읽기 데이터를 삭제하고 이벤트 저장소를 통해서 과거의 모든 이벤트를 다시 실행하여 바뀐 형태의 데이터로 다시 저장하는 것도 가능하다.
CQRS 패턴과 이벤트 소싱 패턴을 같이 사용할 때는 몇 가지 고려사항이 있다.
•
두 패턴을 같이 사용하게 되면 시스템의 복잡성을 증가시키고, 그로 인해 구현을 더 어렵게 만들 수 있다.
•
읽기/쓰기 저장소가 분리되어 있는 시스템에서는 이벤트가 실행되어 저장소가 수정되기 전까지 약간의 딜레이가 발생할 수 있다.
•
읽기 모델에서 데이터를 역정규화하여 저장하는데 시간이 오래 걸리거나 추가적인 리소스가 필요할 수 있다.
Spring에는 이벤트 처리 방식으로 다음 4가지의 기능을 제공한다. 아래로 갈수록 추상화 레벨이 낮아진다.
•
JPA EntityListener
◦
@Entity 혹은 @MappedSuperclass 객체 메서드에 애노테이션으로 지정 가능
◦
@PrePersist, @PostPersist, @PreUpdate 등 7가지 상황에 대해 Callback 함수 지정 가능
◦
해당 엔티티로만 인자를 반환하기 때문에 데이터가 어떤 값에서 어떻게 바뀌었는지 알 수 없음
•
Hibernate EventListener
◦
SeesionFactoryImpl → SessionFactoryServiceRegistry → EventListenerRegistry
◦
26가지 디테일한 상황에 Callback 함수 지정 가능
◦
받고자 하는 상황에 따른 인터페이스를 구현한 클래스를 이벤트로 등록
◦
변경된 프로퍼티, 이전 상태, 현재 상태 등 보다 상세한 정보 전달 가능
◦
모든 엔티티 변경 사항이 전달 됨
•
Hibernate Interceptor
◦
Session 혹은 SessionFactory에 Interceptor 등록 가능
◦
EventListener에 비해 적은 Callback 종류
◦
강력한 기능이지만 위험한 기능(저장될 데이터 조작 가능)
•
Spring AOP
◦
Method에만 설정 가능
◦
Method 실행 전/후, 반환 후, 예외 상황 등에 Callback 지정 가능
◦
Pointcut 문법으로 동작
배달의 민족 B마트에서는 위 기능들 중 Hibernate EventListener로 다양한 api와 이벤트를 callback으로 받아 처리하되, delete 배치와 같이 이벤트를 받지 못하는 경우에만 Spring AOP를 통해 처리했다. 이벤트 발행의 경우 Spring boot와 Amazon의 SNS와 SQS를 통해 처리했다.
SNS
Simple Notification Service의 약자로, 생산자에서 소비자로 메세지를 전달해는 관리형 서비스이다. 구독 중인 여러 서비스에 비동기식으로 통신하여 메세지를 전달한다.
SQS
Simple Queue Service의 약자로, 일종의 대기열을 제공하는 서비스이다. 분산 시스템이나 구성 요소를 통합 및 분리할 때 사용된다.
발행된 이벤트의 처리의 경우 중복된 이벤트의 경우 반복적으로 처리하는게 비효율적이기 때문에, 이벤트 로깅 → Redis 버퍼 저장 → 10초마다 스케줄러로 데이터 일괄 처리 순으로 중복된 이벤트를 배제하며 작업을 수행한다.