배달의민족 주문시스템은?


CQRS 패턴이란?
- CQRS (Command and Query Responsibility Segregation) 패턴은 별도의 인터페이스를 사용해 데이터를 업데이트하는 작업(Command)에서 데이터를 읽는 작업(Query)를 분리하는 패턴입니다.
- 일반적인 시스템에서는 읽기와 쓰기 작업에 동일한 EntityModel 을 사용합니다

- 전통적인 CRUD 접근 방식에는 아래와 같은 몇가지 단점이 있습니다.
- 저장소의 데이터 갱신(CUD)하는 부하뿐만 아니라 정보를 조회(R)하는데 필요한 쿼리의 복잡성으로 인해 성능에 좋지 않은 영향을 미칠 수 있습니다.
- 조회 전용 화면을 구성하기 위해서 불필요한 엔티티 조회가 중복으로 발생합니다.
- 모델 분리
- Data Store에 Command 와 Query Model을 분리된 계층으로 나누는 방식
- 데이터베이스는 분리하지 않고, Model Layer 부분만을 분리
- 조회와 읽기 모델이 분리되어 있어 상황에 맞게 구현이 가능
- 그러나, 동일한 데이터베이스를 사용하기에 성능상 문제는 개선이 어렵다.

- 읽기 전용 저장소 분리
- Command용 데이터 베이스와 Query용 데이터 베이스를 분리하고 둘간을 동기화 처리하는 방식
- Command용 데이터베이스는 정규화된 데이터 베이스를 사용하여 트랜잭션을 보장하고
- Query용 모델은 NoSQL 을 이용하여 조회에 최적화된 설계를 가져갈 수 있다.
- 동기화 처리를위한 처리가 복잡하다.

왜 CQRS를 도입했을까?
- 일평균 250~300만건의 주문 발생
- 데이터양이 많아지며, 조인연산등 복잡한 쿼리에 대해서 성능 부하가 심해짐
- 조회에 필요한 데이터를 최대한 펼쳐서 싱글 도큐먼트로 구성하자

몽고DB는 조인 연산을 지원하지 않기 때문에, 도큐먼트 설계시, 일대다,일대일 관계를 임배딩 도큐먼트 혹은 래퍼런스 도큐먼트로 표현이 가능합니다.
몽고DB에서는 아래와 같은 기준으로 임배딩 도큐먼트, 래퍼런스 도큐먼트를 사용하기를 권장하고 있습니다.
- 임배딩 도큐먼트 : 도큐먼트가 스냅샷 형태로 변경이 없는 도큐먼트의 경우 임배딩 도큐먼트 사용
- 래퍼런스 도큐먼트 : 도큐먼트의 변화가 있거나, 루트 도큐먼트를 통하지 않고 접근이 가능한경우 사용
주문 애그리거트의 도큐먼트들은 모두 스냅샷 형태로 저장되어 주문 생성 이후에는 변경이 없다고 판단하여, 임배디드 도큐먼트로 품는것을 기본으로 생각하였습니다.

어떻게 구현했을까?



// FinderFactory @Component @RequiredArgsConstructor public class ReadOrderFinderFactory { private final MongoReadOrderFinder mongoReadOrderFinder; private final LeaveMongoReadOrderFinder leaveMongoReadOrderFinder; private final RdsReaderReadOrderFinder rdsReaderReadOrderFinder; private final RdsWriterReadOrderFinder rdsWriterReadOrderFinder; public ReadOrderFinder get(ReadOrderDatabase database) { switch (database) { case MONGO: return mongoReadOrderFinder; case MONGO_LEAVE: return leaveMongoReadOrderFinder; case RDS_READER: return rdsReaderReadOrderFinder; case RDS_WRITER: return rdsWriterReadOrderFinder; } throw new IllegalArgumentException("정의되지 않은 조회모델용 database 입니다."); } } // Finder @Repository public class MongoReadOrderFinder extends AbstractMongoReadOrderFinder { public MongoReadOrderFinder(@Qualifier(LiveOrderMongoConfig.MONGO_TEMPLATE) MongoTemplate mongoTemplate, MongoReadOrderConverter readOrderAdapter) { super(mongoTemplate, readOrderAdapter); } @Override public Optional<ReadOrder> findByOrderNo(String orderNo) { Criteria criteria = MongoCriteriaBuilder.create(Operator.AND) .is("orderNo", orderNo) .build(); Query query = MongoQueryBuilder.create() .criteria(criteria) .build(); return Optional.ofNullable(mongoTemplate.findOne(query, HistoryOrder.class)) .map(readOrderAdapter::convert); } } // Finder @Repository @Transactional( value = DomainJpaConfig.TX_MANAGER, propagation = Propagation.REQUIRES_NEW, readOnly = true // reader ) public class RdsReaderReadOrderFinder extends AbstractRdsReadOrderFinder { public RdsReaderReadOrderFinder(OrderRepository orderRepository, RdsReadOrderConverter converter) { super(orderRepository, converter); } @Override public Optional<ReadOrder> findByOrderNo(String orderNo) { return orderRepository.findByOrderNo(orderNo) .map(converter::convert); } } // Application @Slf4j @Service @RequiredArgsConstructor public class OrderHistoryAppService { private final ReadOrderFinderFactory factory; @Override public ReadOrder getHistoryDetail(String orderNo) { ReadOrder optReadOrder = factory.get(ReadOrderDatabase.MONGO).findByOrderNo(orderNo); return readOrder; } }
결과는?

쓰기용DB와 조회용DB의 분리
- as-is : 주문내역 조회에 대한 부하가 "주문" 비지니스에 영향을 줌
- to-be : 주문내역 조회와 "주문"이
완전히 분리되어 좀더 안정적인 주문 기능 제공