CQRS가 뭐야
CQRS 란
개요
- Command Query Responsibility Segregation (CQRS)
- 읽기 및 쓰기 작업을 별도의 데이터 모델로 분리하는 디자인 패턴
- 읽기 모델, 쓰기 모델을 독립적으로 최적화 가능
--> 단순하게 말하면, 조회와 쓰기가 일어나는 작업을 각각 분리하는것
필요성 및 이점
- 읽기와 쓰기의 분리로 복잡도를 낮출 수 있음
- 복잡도를 높이는 부분은 조회쪽일 확률이 높은데, 조회 전용 도메인이 설계가 된다면 이러한 복잡도를 낮추고, Redis 등 좀 더 조회에 최적화된 저장소를 이용해 성능상의 이점도 가져갈 수 있음
점진적으로 개선해보며 알아보는 CQRS
- 아래와 같은 메소드가 존재한다고 가정해보겠습니다. (단적인 예시이기에 일반적이지 않을 수 있습니다. 또한 해당내용은 동기/비동기 관점이아닌 Command Query 분리의 관점으로 작성된 예시입니다.)
초기 요구사항:
- 고객의 요구사항:
- 게시글을 조회 API를 만들어 주세요! 이때 조회수도 증가시켜주세요!
- A 서비스와 B 서비스에서 쓸거예여, A서비스에서는 게시글 단건조회 B서비스에서는 게시글 내용을 가져와 요약해서 보여줄거예요.
getArticleAndIncreaseViewCount(articleId: number): Article {
-- 게시글 조회
-- 게시글 조회수 증가
-- 게시글 리턴
}
- 해당 메소드는 2가지의 책임이 존재합니다.
- 조회수 증가 프로세스
- 게시글 조회 프로세스
이제 추가 요구사항이 들어오기 시작했습니다.
- 고객의 요구사항: 조회수 증가는 유저의 email당 1번으로 제한해주세요.
- 해당 요구사항을 위해 개발자는 히스토리 테이블을 추가하고 아래의 로직으로 변경하였습니다.
getArticleAndIncreaseViewCount(articleId: number, email: string): Article {
-- 게시글 및 히스토리 조회 -> 수정
-- 히스토리에 해당 eamil로 조회 된적이 있는지 체크 없다면 게시글 조회수 증가 -> 수정
-- 게시글 리턴
}
요구사항 변경 후 B서비스에서 문의가 들어왔습니다. “해당 API 가 동작하지 않습니다. 게시글을 가져와 보여주는 서비스인데 게시글을 가져올 수 없으니, 유저가 우리 서비스에서 아무것도 못해요.”
개발자는 그제서야 A서비스에만 eamil을 추가로 받는다는 사실을 알린것을 기억해 냈습니다.
빠르게 B서비스에도 알려 정상화 시켰습니다.
몇일 뒤 보안적인 요구사항이 들어왔습니다.
- 고객의 요구사항: 게시글에 작성자 정보는 제거해 해주세요.
getArticleAndIncreaseViewCount(articleId: number, email: string): Article {
-- 작성자 정보는 제거된 게시글 및 히스토리 조회 -> 수정
-- 히스토리에 해당 email로 해당 게시글이 조회된적이 있는지 체크, 없다면 게시글 조회수 증가
-- 게시글 리턴
}
그리도 또 다시 몇일 뒤 추가 요구사항이 들어왔습니다.
- 추가 요구사항: 조회하는 사람이 작성자면 조회수 증가는하지 말아주세요.
getArticleAndIncreaseViewCount(articleId: number, email: string): Article {
-- 작성자 정보를 다시 포함한 게시글 및 히스토리 조회 -> 수정
-- 히스토리에 해당 email로 해당 게시글이 조회된적이 있는지 체크, 없다면 게시글 조회수 증가
-- 작성자 정보 제거 -> 추가
-- 게시글 리턴
}
그리고 또 다시 요구사항이 들어옵니다.
해당 요구사항으로 또 다시 우리는 getArticleAndIncreaseViewCount 함수를 수정해야합니다. 이렇게 계속 요구사항이 추가되면 getArticleAndIncreaseViewCount 함수는 점점 비대해지고 복잡해질 것 입니다.
그리고 이러한 수정사항을 A서비스에는 알렸지만 B서비스에는 알리지 않았다면 B서비스는 게시글 자체의 조회가 되지 않을 것입니다.
메소드 분리 시작
개발자는 두가지의 요구사항이 한곳에서 관리됨이 불편하여 CQS를 적용하게 되었습니다.
- getArticleAndIncreaseViewCount 메소드를 → getArticle 함수인 Query 메소드와 IncreaseViewCount 함수인 Command 메소드 두개로 분리
이러한 변경사항을 A와 B서비스에도 알렸고, 이제 조회수 증가 요구사항이 바뀌어도 조회 비즈니스에는 영향을 끼치지 않을것이라, 개발자는 한결 마음이 편해졌습니다.
CQS는 Command Query Separation의 약자로, 소프트웨어 개발에서 명령과 조회를 분리하는 설계 원칙입니다.
- 모든 객체의 메소드 작업을 수행하는 Command와 데이터를 반환하는 Query로 구분합니다.
- Command는 결과를 반환하지 않고 시스템의 상태를 변경합니다.
- Query는 결과를 반환하고 시스템의 관찰 가능한 상태를 변경하지 않습니다.
몇일 뒤 문의가 들어왔습니다.
문의사항: 단순 게시글 조회 API인데 좀 더 빠를 수 없나요?
API 핸들러 분리
개발자는 문의사항을 반영하기 위해 조회 API와 조회 수 증가 API를 분리하기로 마음먹게됩니다.
- Command 클래스 / Query 클래스 분리
- REST 기준 POST, PUT, DELETE → Command / GET → Query 로 API 핸들러 분리
이제 조회 API는 조회수 증가로부터 완전히 자유로워졌습니다.
그런데 문제가 생겼습니다.
문의사항: 유저수가 폭증했는데 API가 작동을 안해요! 조회라도되게 해주세요!
확인해보니 서버의 리소스가 폭증하였고 서버는 죽어버렸습니다.
서버의 분리
- Command 서버
- Query 서버 분리 후 스팩 향상
서버의 리소스를 API 조회에 치중시켜 개발자는 한결 마음이 편해졌습니다.
그런데 문제가 생겼습니다. 유저 수의 폭증으로 서버는 죽지않았지만 데이터베이스에서 병목이 생겼습니다.
독립된 Database로의 분리
- DB 리소스또한 분리를 위해 Command 서버는 Master DB를 Query 서버는 Slave DB를 연결
이제 각각의 Command와 Query에 맞게 시스템의 리소스까지 분리하게되었습니다. 개발자는 왠만한 성능 이슈에서는 자유로워질 수 있었습니다.
더 최적화 시키는 Query DB
개발자는 조회성능을 높이기위해 Read Model을 구성하고 Qeury 서버를 Redis나 NoSQL 기반으로 바꿀 수 있습니다.
또한 분리가 안되는 컨텍스트 바운드에서는 이벤트 발행을 통해 Command와 Qeury를 분리할 수 있습니다.
이러한 작업을 수월하게 하기 위해서는 Command 와 Query에 맞는 도메인 설계가 이루어져야 합니다.
결론
- CQRS의 핵심은 Command Query 책임을 명확히 나누고 독립적으로 확장 가능한 시스템을 만드는 것