Design Patterns

마이크로 서비스패턴 - 7장. 마이크로서비스 쿼리 구현

FreeEnd 2022. 4. 7. 00:49
반응형

이 블로그는 마이크로서비스패턴 (길벗) 책 내용을 스터디를 위해 정리 한 내용 입니다.

책구매는 바로가기 에서 구매 가능합니다.

 

마이크로 서비스에서 쿼리는 API조합패턴과 CQRS 패턴으로 쿼리를 구현한다. 이번장에서는 이 두 API 에 대해 설명 한다.

 

API 조합 패턴 응용 쿼리

 

findOrder() 쿼리

 모놀리식 시스템은 전체 데이터가 하나의 DB에 있기 때문에 SELECT 문으로 쉽게 여러 테이블 데이터를 조회할 수 있다. 하지만 마이크로 서비스 어키텍처는 데이터가 여러 시스템에 뿔뿔이 흩어져 있기 때문에 조회가 어렵다.

 

 주문ID를 기준으로 주문을 조회하여 주문 상세를 return 하는 findOrder() 가 있다고 하자. 다양한 정보를 모아 return 해주어야 한다. 이 시스템들이 여러군데 흩어져 있으니, 어떻게 조회를 해야 할까.

 

 

 

API 조합 패턴 개요

 API 조합 패턴은 데이터를 가진 여러 프로바이더 서비스에 API 로 호출후 결과값을 가져와 조합한다.

 

  • API 조합기 : 프로바이더 서비스를 쿼리하여 데이터를 조회
  • 프로바이더 서비스 : 최종 결과로 반환할 데이터를 갖고 있는 서비스

 

이 패턴으로 특정 쿼리작업을 구현 가능할지는, 프로바이더 서비스의 상황과 데이터가 어떻게 분할되어 있는지, 어떻게 데이터를 제공하는지에 따라 다르다. 하지만 대부분의 경우 충분히 이 패턴으로 데이터를 조회 할 수 있다.

 

 

API 를 조합 패턴으로 findOrder() 쿼리 구현

findOrder() 는 주문ID를 기준으로 데이터를 조회하는 METHOD 이기 때문에, 마이크로 서비스의 경우 특정 키를 기준으로 데이터 조회가 가능하기 때문에, API 조합패턴을 구현하기 수월하다.

 

(참고, EQUI JOIN 은 두 테이블 에서 공통적으로 존재하는 컬럼의 값이 일치되는 행을 연결해 결과를 생성하는 JOIN 방식)

 

REST API 등의 인터페이스를 이용해 각 서비스의 데이터를 키기준으로 조회한다. 

 

 

 

API 조합 설계 이슈

API 조합패턴에는 다음 두가지 설계 이슈가 존재한다.

  • 어떤 컴포넌트를 API 조합기로 선정할것인가?
  • 어떻게 조회한 값을 효과적으로 취합할것인가?

 

누가 API 조합기 역할을 맡을 것인가?

  1. 서비스 클라이언트 (어플리케이션) 를 API 조합기로 임명
    실제 서비스가 되는 클라이언트가 각 API 를 호출해 취합된 데이터를 표현 하게 개발. 단, 서비스가 다른 네트워크 영역에 존재할 경우 성능의 이슈가 있을 수 있음.

  2. 애플리케이션의 외부 API 가 구현된 API 게이트웨이를 API 조합기로 임명
    클라이언트 어플리케이션은 API 게이트웨이를 호출하고, API 게이트웨이가 각 MSA 서비스를 호출해 데이터를 취합, 한번에 클라이언트에게 결과값을 전달함.

  3. API 조합기를 Stand-Alone Application 으로 구현
    API 게이트웨이로 구현이 힘든 복잡한 쿼리 작업이라면 스탠드어론 어플리케이션을 개발해 해당 서비스로 각 MSA 서비스를 호출해 데이터를 취합, 한번에 클라이언트에게 결과값을 전달함.

API 조합기는 리액티브 프로그래밍 모델을 사용해야 한다.

 쿼리 작업시 다양한서비스를 순서대로 각각 호출하면 시간이 너무 많이 걸려, 가능하다면 병렬 처리를 하여 시간은 단축 해야 한다.  하지만 일부 서비스는 먼저 호출해 결과값을 받은다음 그 받은 값으로 다음 서비스를 호출해야 하는 경우도 있어 프로바이더를 어쩔수 없이 순차 호출해야 할 수도 있다. 

 하지만 병렬/순차 방식이 뒤섞이면 로직이 복잡해 질수 있다. 때문에 CompletableFuture, RxJava의 옵저버블, 등의 리액티브 설계 기법을 도입 해야한다.

 

 

API 조합 패턴의 장단점

api 조합 패턴은 다음과 같은 단점들이 존재한다.

 

오버헤드가 증가한다.

모놀리식 어플리케이션은 클라이언트가 한번요청으로 처리가 가능하지만, 마이크로서비스의 경우 API 를 여러번 호출 해야 하기 때문에 리소스가 더 많이 소모되고 운영 비용도 늘어난다.

 

가용성이 저하될 우려가 있다.

 적어도 하나의 API 조합기에 두개 이상의 프로바이더 서비스가 반드시 존재해야 하는 구조로 가용성이 낮다. 

가용성을 높이기 위해서는 일부 프로바이더 서비스가 불능 상태일 경우

첫째, 캐시를 이용해 이전에 조회한 값을 반환한다.

둘째, API 조합기가 판단하기에 중요하지 않은 정보는 제외하고 미완성된 데이터를 반환한다. 

 

데이터 일관성이 결여된다

  여러 서비스에 변경데이터가 전파되기 전에 조회가 된다면, 일부서비스는 최신의 데이터를 반환할거고, 일부 서비스는 아직 갱신 되지 않은 상태의 데이터를 반환할 것이다. 

 

 

CQRS 패턴

 엔터프라이즈 어플리케이션은 대부분 RDMS 에서 레코드를 관리하고, 텍스트 검색등은 일래스틸서치나 솔라등 텍스트 검색 DB를 많이 이용한다. 다양한 DB의 장점을 두루 사용하는 것이다.

 CQRS 는 이런 종류의 아키텍처를 일반화 한것이다. 하나이상의 쿼리가 구현된 하나이상의 뷰 DB를 유지하는 방식이다.

 

CQRS의 필요성

API 조합 패턴으로 구현하기 어려운 서비스들이 있다. 

 

findOrderHistory() 쿼리 구현

findOrderHistory는 다음 두 매개변수로 주문이력을 반환하는 쿼리라고 하자.

  • consumerId : 소비자ID
  • OrderHistoryFilter : 주문 이력 필터조건, 기간/상태/키워드 등.

findOrderHistory는 findOrder 와 다르게 다건 목록, 즉 리스트를 반환하는 기능이다. 이렇게 리스트형 데이터를 필터 조건으로 제한하여 반환 할 경우에 서비스 프로바이더가 여러개일경우, 특정 프로바이더 서비스만 적용필터 조건이 적용될 수 있다면 곤란한 상황에 이를수 있다.

이는 다음 방식으로 해결 가능한다.

  1. 첫번쨰 방식은, API 조합기로 데이터를 인메모리 조인을 한다. 즉, 모든 데이터를 각 프로바이더 서비스를 이용해 조회한 다음 필터 조건이 가능한 서비스는 필터를 적용하고, 나머지 프로바이더 서비스의 데이터와 JOIN 하는 방식이다.
  2. 두번째 방식은, 필터 조건이 가능한 프로바이더 서비스를 먼저 조회하고 조회결과를 이용해 그 다음 서비스를 호출 해 데이터를 취합한다. 단, 해당 서비스가 대량 조회를 제공 해야 가능하다.

두 방식 모두 비효율 적이다.

 

어려운 단일 쿼리 서비스

데이터를 가진 서비스에 쿼리를 구현하는경우가 부적절한 경우?

서비스DB가 효율적인 쿼리를 지원하지 않는경우.

특정 지역을 조회해야 하는 서비스일경우 MongoDB 나 MySQL, Postgres 같은 DB를 이용하면 지리공간INDEX 등을 이용해 특정위치의 지역데이터를 쉽게 추출할 수 있지만 현재 서비스를 사용하는 DB가 지원하지 않는다면, 구현하기 어렵다. 이때 레플리카 등을 이용해 다른 DB를 이용하게 만들면 된다.

 

 

관심사를 분리할 필요성

어떨때는 데이터를 가진 서비스에 쿼리를 구현하면 안될 경우도 있다. 특정 서비스에 너무 많은 책임을 부과하지 않도록 고민 해야한다. 

 

 

CQRS 개요

마이크로 서비스 아키텍처에서 쿼리 구현시 자주 만나는 이슈

  • API 를 조합해 데이터를 만들어낼 경우, 비효율적인 인메모리 조인이 필요하다
  • 필요한 쿼리를 효율적으로 제공하지 않은 DB를 사용할 경우 (지리공간 조회기능등)
  • 관심사 분리가 되지 않아 안쪽의 책임이 너무 높게 부여된 경우

이런 이슈를 해결 할 수 있는 방식이 CQRS패턴이다.

 

CQRS 는 커맨드와 쿼리를 서로 분리한다.

CQRS 는 이름 그대로 관심사를 분리한 패턴이다. 

조회기능은 QUERY 에, 생성/수정/삭제는 COMMAND 에 구현한다. 두 모델 사이의 동기화는 COMMAND 의 이벤트를 QUERY 에서 구독하는 방식으로 구현된다.

COMMAND 는 변경사항을 자신의 DB에 업데이트 하고 QUERY  는 변경된 이벤트를 수신해 자체 DB에 데이터를 동기화한다. 이때 QUERY 는 다양한 DB형태로 유지 될 수 있다.

 

CQRS 와 쿼리 전용 서비스

쿼리서비스는 커맨드 작업이 없는 단순히 조회 결과를 제공하는 API 와, COMMAND 로 부터 변경사항을 전달 받아 데이터를 항상 최신으로 유지하는 로직으로 구현되어 있다.

  쿼리 서비스는 특정 서비스에 종속되지 않도록 별도의 서비스(Stand-Alone) 로 개발 되어야 한다.

 

CQRS의 장점

마이크로 서비스 아키텍처에서 효율적인 쿼리가 가능하다

API 를 조회해 인메모리 방식으로 JOIN 하는게 아닌, 이미 동기화 하여 미리 조인해둔 QUERY DATABASE 를 이용하는게 훨신 효율적이다.

 

다양한 쿼리를 효율적으로 구현할 수 있다.

단일 모델이 아닌, 다양한 종류의 DB를 사용해 다양하게 쿼리 할수 있어 단일 저장소 의 한계를 극복 할 수 있다.

 

이벤트 소싱 애플리케이션에서 쿼리가 가능하다.

CQRS 는 하나이상의 애그리거트 뷰를 정의해, 이벤트 소싱기반의 이벤트스트림을 구독해 최신상태를 유지한다. 이벤트 소싱 방식에 가장 알맞는 패턴이다.

 

관심사가 더 분리된다

커맨드, 쿼리에 알맞는 로직이 분리되어 관리하기 편해진다. 

 

CQRS의 단점

아키텍처가 복잡하다

 뷰를 조회/수정하는 쿼리 서비스를 별도로 만들어야 하며, 별도 저장소도 관리 해야한다.

 

복제시차를 신경 써야 한다.

커맨트 / 쿼리 데이터의 동기화 시차가 발생하기 때문에 해당 부분을 고려해 개발 해야 한다.

 

 

 

 

 

CQRS 뷰설계

CQRS 뷰 설계시 고려할 사항

  •  DB 를 선정하고 스키마를 설계해야한다.
  • 데이터 접근 모듈 설계시 멱등/동시 업데이트 등에 대해 고려해야 한다.
  • 스키마 변경시 뷰를 효율적으로 재 빌드(적용) 할 방법을 강구해야 한다.
  • COMMAND 로 변경된 데이터를 QUERY 에 복제하는 시차를 어떻게 처리해야 할지 결정해야한다.

뷰DB선택

SQL 대 NoSql

NoSQL 은 트랜잭션기능이 제한적이고, 범용적 쿼리 사용이 힘들지만, 유연한 데이터 모델고 우수한 확장성때문에  뷰DB와 잘 맞는 편이다.

 

업데이트 작업 지원

 뷰 데이너 모델은 커맨드의 이벤트를 수신해 업데이트 작업을 해야 하는데, 효율적인 구현이 필요하다. 업데이트가 필요한  이벤트가 실제 저장된 데이터와 1:1 이라면 쉽게 업데이트가 가능하겠지만, 여러개의 데이터라던지, 주어진 pk가 아니라면 업데이트가 어려울수 있다.

MongoDB는 PK가 아니라 다른 컬럼도 INDEX 를 줄수 있으므로, 이렇게 INDEX 등을 제공하는 NoSQL DB라면 효율적으로 데이터를 찾아 업데이트 할 수 있다.

 

데이터 접근 모듈 설계

뷰DB 에서 데이터 접근은 DAO 가 담당한다. 이 DAO 에서 처리 해야 하는 일이 많다.

 

동시성 처리

 뷰가 여러개의 애그리거트가 발행한 이벤트를 구독할 경우, 여러 이벤트가 하나의 레코드를 업데이트 하는 경우가 발생 할 수 있다.

 레코드 수정시, 해당 레코드를 조회한 다음 데이터를 수정하고 다시 레코드를 작성하면, 문제가 생길수 있다. 이때는 낙관적 / 비관적 잠금을 적용하던지, 아니면 조회절차없이 DB에 그대로 데이터를 해야 한다.

 

멱등한 이벤트 핸들러

 이벤트 핸들러는 같은 이벤트를 한번 이상 넘겨받고 호출 될 수 있다. 때문에 멱등성을 보장 해야한다. 동일 데이터를 업데이트 해도 이슈가 없는 시스템 이라면 문제가 없지만, 비멱등적 이벤트일 경우에는 이미 처리한 이벤트키를 저장해두는 식으로 중복 처리에 대비 해야한다.

 

클라이언트 애플리케이션이 최종 일관된 뷰를 사용할 수 있다.

CQRS 적용시 커맨드쪽을 업데이트 한뒤 바로  VIEW 를 조회하면 동기화되지 않았기 때문에 자신이 업데이트한 내용을 못보게 될 가능성이 있다. 

 커맨드와 쿼리 모듈 API 를 이용하면 클라이언트가 비일관성을 감지 할수 있다. 클라이언트가 커맨드쪽 작업시 커맨드가 발행해준 이벤트ID를 반환하고, 저장즉시 쿼리 조회시 이 이벤트ID를 전달하면 해당 이벤트로 인해 VIEW 가 업데이트 되지 않았을경우, 에러가 반환될 것이다.

 

 

CQRS 뷰 추가 및 업데이트

 뷰를 운영할때 시간이 지나면서 새로운 뷰를 구축하거나, 수정해야 할 경우가 생긴다. 이때 새 뷰생성시 쿼리쪽 모듈을 개발 / 저장소세팅 / 서비스를 배포 해야 한다. 

 

아카이빙된 이벤트를 이용하여 CQRS 뷰 구축

 메시지 브로커는 메시지를 무기한 저장 할수 없다. 때문에 메시지 브로커를 통해 초기 적제는 어려울수 있다. 

이럴때는 AWS S3 등의 아카이빙된, 더 오래된 이벤트를 가져올 방법을 찾아야 한다.

 

CQRS 뷰를 단계적으로 구축

 뷰 데이터가 늘어나면서 점점 시간/리소스가 줄어드는것도 문제이다. 이벤트 소싱 같은 방식은 중간에 스냅을 두는 방식으로 해결한다.

 

 

반응형