단일 모델의 단점
주문 내역 조회 기능을 구현한다고 가정한다. 주문 내역은 여러 애그리거트의 정보를 가져와야 한다. Order에서 주문 정보, Product에서 상품 정보, Member에서 회원 정보, Payment에서 결제 정보 등 여러 애그리거트의 정보를 한 데 모아 응답해줘야 한다.
이렇게 조회를 위해 여러 애그리거트의 데이터가 필요한 경우 구현 방법을 고려해봐야 한다. 애그리거트를 사용하는 경우 각 애그리거트는 애그리거트ID 라는 식별자를 통해 참조하기에 Eager Loading
전략을 사용할 수 없다. 이는 한번의 SELECT를 통해 조회에 필요한 모든 데이터를 불러올 수 없다는 의미이므로, 무의미한 I/O를 발생시켜 성능 오버헤드가 발생할 수 있다.
그렇다고 애그리거트간 참조를 객체 레퍼런스를 통해 구현해버리면 애그리거트에서 다른 애그리거트의 상태를 변경할 수 있는 가능성을 열어두는 꼴이고, 추후 마이크로서비스 아키텍처로 마이그레이션하는데 걸림돌이 될 가능성이 매우 크다.
이런 문제가 발생하는 이유는 도메인의 상태를 변경할 때와 조회할 때 모두 단일 도메인 모델(애그리거트)을 사용하기 때문이다. 객체 지향으로 도메인 모델을 구현할 때 주로 사용하는 ORM 기법은 도메인의 상태 변경 기능을 구현하는 데는 적합하지만 단순히 데이터를 조회할 때는 로딩 전략등 고려해야 할 점이 많아 구현을 복잡하게 만드는 원인이 되기도 한다. 구현 복잡도를 낮추기위해 상태 변경과 조회에서 다른 데이터 모델을 사용하는 것은 어떨까?
CQRS
시스템이 제공하는 기능은 크게 조회와 수정으로 나눌 수 있다. 웹 애플리케이션은 대부분 수정 기능보다 조회 기능이 많고, 빈번하게 호출된다. 또한 수정을 위한 데이터와 조회를 위한 데이터가 다른 경우가 많다. 도메인 모델이 복잡한 경우 도메인 수정을 위한 Command 와 조회를 위한 Query 기능을 분리하면 성능 향상에 도움이 될 수 있다.
CQRS(Command Query Responsibility Segregation)는 기존에 하나로 사용하던 도메인 모델을 도메인의 상태를 변경하는 Command 모델과 도메인의 상태 데이터를 제공하는 Query 모델로 분리하는 패턴이다.
Command 기능은 DDD를 기반으로 애그리거트를 통해 구현한다. 조회기능은 일반적으로 수정 기능에 비해 더 많은 테이블을 조인해서 데이터를 가져와야 하는 경우가 많다. 따라서 Query 기능은 조회 정보를 매핑하기 위한 모델을 따로 생성하고 데이터를 매핑한다. Query를 처리하기 위한 별도의 데이터베이스를 두기도 하며, MySQL의 경우 Master, Slave 관계를 통해 DB 인스턴스를 분리하기도 한다.
Command 를 처리함에 있어 Lazy Loading, 더티 체킹등의 기능이 필요하여 JPA를 사용했다고 해서 Query 기능도 JPA를 사용하란 법은 없다. 만약 JPA의 기능이 필요없고 단지 여러 테이블을 조인하여 데이터를 가져오는 기능만을 가진다면 MyBatis를 통해 구현하는 것이 더 효율적일 수 있다.
MSA - Query스탠드 얼론 서비스
MSA의 경우 Query 기능 처리를 위한 스탠드얼론 서비스를 두기도 한다.
자세한 내용은 MSA 관련 내용을 정리할 때 함께 정리하려 한다.
데이터베이스 동기화 문제
Command 와 Query 모델이 각기 다른 데이터베이스를 사용하는 경우 두 데이터베이스의 데이터를 동기화시켜야 한다. Command 모델에서 도메인 모델의 데이터를 변경하는 것과 이벤트를 발행하는 것을 하나의 트랜잭션으로 묶어 처리한다. Query 모델은 Command 모델에서 발행하는 이벤트를 구독하고 이벤트에 맞게 Query 데이터베이스(이하 뷰)의 데이터를 최신화 하면 된다.
여기서 문제는 동기화 시점을 언제 가져가냐이다. 도메인의 상태 변경과 해당 변경을 뷰에 반영하는 작업을 동기적으로 처리해야하는 기능이 있는 반면 비동기적으로 처리해도 문제가 되지 않는 기능도 있다. 동기적으로 처리해야 한다면 동기 이벤트와 글로벌 트랜잭션을 사용하여 처리할 수 있지만 전반적인 성능이 떨어진다는 단점이 존재한다. 비동기적으로 처리해도 되는 기능은 비동기 이벤트 통신을 통해 구현하면 된다.
CQRS 와 캐싱
Commnad 와 Query를 분리하는 것은 데이터 캐시에도 효율적일 수 있다. 앞서 말했듯이 뷰는 여러 테이블을 조인한 결과가 필요한 경우가 대부분이고, 여러 테이블의 데이터를 그대로 캐시저장소에 저장하는 것은 조회 시 인-메모리 조인을 발생하여 성능 오버헤드가 발생할 가능성이 존재한다.
이런 경우 여러 테이블을 조인한 결과를 매핑한 객체(뷰 전용 데이터) 자체를 캐시 저장소에 저장해놓는 방법을 고려해볼 수 있다. DB에 보관된 데이터를 그대로 메모리에 저장하기보다는 화면에 맞는 모양으로 변환한 데이터를 캐싱하는 것이 성능에 더 유리할 것이다.
단점
하나의 모델로 처리하던 기능을 Command와 Query 모델로 분리한다면 단순히 구현해야 할 코드의 양이 많아진다. 단일 모델로 처리할 때의 복잡함을 조회 전용 모델을 구현함으로써 해결할 순 있지만 다른 모델을 구현하며 발생하는 비용을 고려해야 한다.
조회를 위한 데이터베이스를 두거나, 새로운 ORM 기술을 사용한다면 조회 기능을 구현하기 위한 기술이 추가로 필요하게 된다. Command DB와 뷰의 동기화를 위해 메시징 시스템을 도입해야 할 수도 있다.
Reference
- 도메인주도 개발 시작하기:DDD 핵심 개념 정리부터 구현까지(최범균, 2022)
- 마이크로서비스 패턴(크리스 리처드슨)
'CS' 카테고리의 다른 글
Rate Limiter 와 구현 알고리즘 (1) | 2024.04.25 |
---|---|
DDD - 외부 시스템 호출 분리와 이벤트 (0) | 2024.04.06 |
DDD - 애그리거트 트랜잭션 관리와 동시성 처리 (0) | 2024.04.05 |
DDD - 도메인과 애그리거트 (1) | 2024.03.27 |
Blocking vs Non-Blocking I/O (0) | 2024.03.26 |