본문 바로가기
MSA

CQRS 패턴 적용기 - 배민 우아콘 정리

by notcherry 2023. 12. 9.

 

 

MSA로 프로젝트를 진행하며 한 레포지에 조회와 생성 메서드가 다 들어가있으니 가독성도 떨어지고 DB를 직접 조회하는 것에 대하여 부담을 느꼈다. 서비스 구현이 많지 않은데도 불구하고 벌써부터 이런 문제가 생기면 안 되겠다 싶어 찾아보니 CQRS 패턴을 알게 되었고, 배민에 괜찮은 강의가 있어 보고 정리하였다. 확장성을 고려해 CQRS 패턴을 꼭 도입하고 싶은데, 다음엔 직접 적용한 경험을 기록하겠다!

 

1.B마트를 운영하며 마주한 문제

RDBMS 기반의 서비스들이 그렇듯 아주 많은 서비스가 데이터를 관리하는 차원에서 정규화한 데이터와 노출 도메인의 데이터 구조가 다르다. B마트도 MySQL을 이용하고 있으며 지점, 카탈로그, 상품에 대한 메타 정보를 입력, 수정, 노출하며 이를 위한 매핑 설정을 최종 단계에서 따로 진행하기 때문에 데이터 스키마는 정규화되어 있다. 전시 도메인에서는 이를 노출하기 위해 비정규화하기 위한 작업이 필요했다.

 

2.CQRS 정의와 도입 배경

Command and Query Responsibility Segregation 의 약자로, 명령과 조회의 책임을 분리한다는 뜻이다. 주로 UX와 비즈니스 요구사항이 복잡하거나 조회 성능을 높이고 싶을 때, 데이터를 관리하는 영역과 이를 뷰로 전달하는 영역의 책임이 나뉘어야 할 때, 시스템 확장성을 높이고 싶을 때 사용한다.

B마트에서는 비즈니스 로직은 분리되어 있지만 모델은 하나의 모델이 명령과 조회를 수행하고 있었다. 모델은 내부에서만 사용되는 관리용 데이터나 성능상 이슈 등으로 실제 조회에는 이용되지 않는 여러 데이터들을 포함하고 있다. 또한 노출의 측면에서는, 정책사항과 전시를 위해 재고 정보, 배달 수단과 같이 여러 API 로부터 주입받는 정보가 생기게 되어 이로 인해 모델이 복잡하게 되는 케이스가 발생했다. 이런상황은 자연스럽게 명령 관련 로직과 조회 관련 로직이 하나의 모델을 바라보게 되고 서로 영향을 주게 된다. 이 문제를 해결하기 위해 명령 도메인과 조회 도메인을 분리하게 되었다.

 

3. 본격적인 도입

명령 관련 로직과 조회 관련 로직을 분리하기 위해 교집합 지점은 모델을 분리하는 작업

  • 명령 모델은 기존 도메인 모델을 뜻한다.
  • 조회 모델은 전시 광고 등 노출해서 이용되는 비정규화된 데이터를 한번 더 정의한 모델이다. 이를 구현하려면 자연스럽게 조회 모델을 생성하는 영역이 생기게 된다. 이때, Entity를 이용하기 보단, DTO를 정의해서 사용하는 것을 추천한다!
  • 조회 모델을 만든다는 것은 최적화한 스키마를 비정규화해야 한다는 것인데 이때 성능 상의 부담을 갖게 된다. 이를 해결하기 위해 중간에 캐시를 추가하는 작업을 수행한다. 매번 조회 모델(DTO)을 생성할 경우 성능 상의 이슈가 발생한다. 성능과의 trade off가 되는힙 메모리 사이즈나 캐시와의 트래픽 양 증가 등의 이슈가 생기는 상황이 발생될 수 밖에 없다! 이를 근본적으로 해결하기 위해서는 단순하게 데이터가 변경되는 시점에 조회 모델을 생성하고 비정규회된 데이터를 그 자체로 저장하는 방법이 있다. 
  • CQRS에서는 조회 모델은 DB로부터 가져오는 과정에서 JOIN과 같은 기타 연산 작업을 극히 제한한다. 가능하면 DB의 값을 그대로 가져와 곧바로 이용 가능한 형태로 제공하는 것을 권장한다. 또한 조회를 위한 새로운 테이블 설계가 필요하다. JSON FORMAT을 주로 사용하기 때문에 RDB가 아닌 NoSQL DB를 많이 사용한다. 조회 시에는 데이터를 그대로 읽어 사용하기 때문에 조회에서 캐시를 사용하지 않고도 근본적인 해결책을 가져갈 수 있다!
  • B마트에서는 조회 성능을 더 끌어올릴 수 있는 Redis를 사용하는데 데이터가 분리되어 존재하기에 가능한 작업이다.
  • 기존 DB에서도 Fallback 상황을 대비해 저장하고 있다. DB를 더 최적화한 DB로 변경했기 때문에 쿼리 성능을 높일 수 있고, 조회 모델을 사용하는 형태에 따라 레디스가 아니더라도 다양한 데이터 저장소를 선택할 수 있는 확장성에도 큰 장점이 있다. 하지만! 나뉜 저장소 간 데이터 정합성을 보장할 수 있어야 하기에 시스템 모니터링에 신경써야 한다는 단점이 있다.
  • 이벤트 소싱 패턴은 어떤 앱으로부터 발생한 이벤트를 event store에 저장하고 이를 여러 시스템이 구독하여 다룰 수 있는 패턴이다. 명령 모델로 변경 감지가 되었을 때, 조회 모델을 생성하기 위한 로직을 그곳에서 작성하는 것이 아니라, 그저 변경된 상태를 변경하고 전달된 상태가 스트림에 쌓이면 해당 테이블을 consume해서 조회 모델을 생성한다. 즉, 조회 모델을 생성하는 시점이 명령 모델의  변경으로부터 이루어질 대, 생성에 책임을 떠안는 쪽(명령 모델에 쌓이는) 부하를 분산할 수 있게 된다.

 

조회 모델 설계하기

  • 최소한의 쿼리로 설계하기 위해 복잡한 로직을 정리하는 것 부터 시작한다. 책임을 분산하고, 의존 관계를 분명하게 해 데이터가 단방향으로 흐를 수 있도록 전체 트리를 정리한다. B마트에서는 카탈로그 목록, 카탈로그, 상품 이렇게 세 가지의 데이터만 필요했고, 이것으로 도메인 영역을 정의했다. 전체 트리 모델에서 의존성을 갖는 모든 로직이 모였던 부분이 결국 세 가지 데이터만 제공하는 형태로 정리가 되면서, 노출 가능 카탈로그 목록 제공, 카탈로그, 상품과 상품 목록 제공으로 세 가지 형태로 설계했다.
  • 조회 모델추출 전에는 전체 트리 모델에 의존하고, 각 로직 별로 캐시에 의존했던 구조가 분리해줌으로써 조회 모델에만 캐시를 적용하기 때문에 캐시 트래픽에 대한 부담도 줄일 수 있었다. 
  • 최종 그림은 명령 모델에서 변경 사항을 이벤트로 전달하는 이벤트 소싱 패턴을 도입하여 완성했다. 팀이 다른 상태에서 명령 모델을 다루는 쪽 코드에 깊게 관여하기 힘들어졌다. 변경 감지할 데이터를 선하는 것이 중요하여, 이를 위해 사용될 인터페이스를 구현했다.
  • 해당 인터페이스를 엔티티가 상속받아 다음과 같은 정보들을 Optional하게 전달해야 한다.
    • 엔티티 변경에 따라 갱신해야할 대상 ID(카탈로그 OR 상품)
      • 해당 엔티티가 변경되었을 때 수정해야 할 대상은 더 상위에 존재할 수 있기에 상품이 변경되었을 때도 카탈로그를 갱신해야 한다던가와 같은 option 부여가 필요했음
    • 변경 감지할 property 지정
      • 이 또한 optional로 진행하며, 모든 속성을 추적하다 보면 필요하지 않음에도 성능에 큰 영향을 주는 경우가 존재할 수 있음
    • 실행 method 지정
      • 특정 메서드 실행시에만 동작하는 것이 성능면에서 이점을 줌
    • 데이터 변경자명
      • 관리 측면에서 어떤 유저가 언제 변경되었는지에 대한 로그성 데이터도 전달받고 있음
      • 변경 내역 전체를 전송하면 조회 모델에서 로직이 복잡해짐. entity Id만 전송하는 zero Payload 방식 도입. persistence 데이터에 바로 접근할 수 있고 해당 데이터로부터 데이터를 또 다시 뽑아 가장 최신 상태를 조회 모델로 생성할 수 있기에 entity Id만 전달하는 구조

 

데이터 변경 감지하기

  • 변경 감지 방법
    •  JPA Entity Listeners
    • Hibernate EventListener
    • Hibernate Interceptor -> B마트에서는 단순한 콜백과 hooking이 필요했기 때문에 사용 안 함.
    • Spring AOP
    • 내려갈수록 추상화 레벨이 낮아짐

이벤트 발행하기

실제로 데이터 변경을 감지했다면, publish 클래스를 소개했듯이 이벤트를 발행해야 한다. 이미 전사적으로 쓰고 있던 환경(Amazon SNS, SQS)을 활용했다. spring-cloud-starter-aws-messaging에 의존성만 추가하면 사용할 수 있다.

 

이벤트 받고 처리하기

AWS-SQS를 사용해 메시지를 수신했다. 매우 간단한 큐 서비스이다. B마트에서는 Custom SQS 메시지 폴링 설정과 Message Converter를 등록했다. 

이벤트에 실어서 보낸 entity Id를 기반으로 원하는 조회 모델을 비정규화해 저장하고 있는데, 이때 비효율적이 부분이 발생했다. 드래그와 같이 메시지가 연속적으로 발생할 때 조회 모델을 갱신하게 되면, DB를 향한 요청 수가 급등하게 된다. 이를 해결하기 위해 전송받은 이벤트를 ELK를 통해 로깅하는 절차를 거치고 레디스를 버퍼로 활용해 이벤트를 저장했다. 스프링 스케줄러를 이용해 10초에 한 번씩 버퍼의 모든 요청을 가져온 후 조회 모델을 벌크로 생성하고 저장했다. 최대한 데이터의 정합성을 보장하기 위해 시간마다 Full Batch를 돌아 모든 조회 모델을 생성하고 저장한다.

'MSA' 카테고리의 다른 글

아틸러리 그래프 부연설명  (0) 2024.03.01
아틸러리를 활용한 스트레스 테스트  (1) 2024.02.28
동시성 처리의 중요성과 방법  (1) 2024.01.26
MSA로 전향한 11번가의 사례  (1) 2023.12.06