【AWS Amplify 노하우 시리즈】 5. List API 는 DynamoDB 로, Search API 는 Elasticsearch 로!

Amplify 시리즈 다섯번째 글입니다! 이번 글에서는 List API 와 Search API 가 서로 다른 데이터소스에 쿼리를 날리게 됨으로써 발생하는 차이점에 대해 알아보고, 각각을 어떤 상황에서 어떻게 사용할 수 있는지에 대해 살펴보겠습니다.
2020.07.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

안녕하세요!ㅎㅎ Classmethod 컨설팅부 소속 김태우입니다.

Amplify 시리즈 다섯번째 글입니다! 이번 글에서는 List API 와 Search API 가 서로 다른 데이터소스에 쿼리를 날리게 됨으로써 발생하는 차이점에 대해 알아보고, 각각을 어떤 상황에서 어떻게 사용할 수 있는지에 대해 살펴보겠습니다.

그럼 시작하겠습니다! :)

리스트는 리스트고 서치는 서치 아닌가??

애초에 List 와 Search 를 왜 비교하려는지 의문을 가지신 분들이 많을 것 같습니다. 애초에 이 둘의 용도는 다른것이니까요. 그런데, Amplify 에서 제공해주는 List API 는 DynamoDB 를 데이터소스로 사용하고, Search API 는 Elasticsearch 를 데이터 소스로 사용한다는 점을 알게되신다면 활용방안에 대해 조금 더 고민해볼 필요가 있다는 점을 눈치채실 수도 있을 것 같습니다.

저는 개인적으로 DynamoDB 를 매우 좋아합니다. Elasticsearch 도 정말 좋아하지만, DynamoDB 와 Elasticsearch 를 조합해서 사용하는 것은 훨씬 더 좋아합니다. 이게 무슨말일까요?

DynamoDB 는 AWS 의 대표적인 KVS(Key-Value Storage) 로, 사용한 만큼에 대해서만 요금이 부과되는 서버리스 데이터베이스 플랫폼으로도 유명합니다. AWS Lambda 를 사용하게되면 default 데이터베이스로 선택되는 서비스이기도 하죠. DynamoDB 는 KVS 의 특성상 어떤 쿼리에 대해서도 수 밀리초(ms) 만에 결과를 가져올 수 있는 뛰어난 퍼포먼스를 보여주는 반면, 복잡한 형태의 쿼리나 aggregation 작업은 거의 지원하지 않습니다. 따라서, JSON 데이터를 DynamoDB 의 하나의 컬럼에 저장하는 것은 가능해도, 이 컬럼의 JSON 내부 필드에 대해 쿼리를 날린다던지, 쿼리대상이 되는 전체 데이터의 수를 count() 해준다던지 하는 기능은 DynamoDB 에서 제공하고 있지 않습니다.

하지만, DynamoDB 는 Streams 라고 하는 강력한 기능을 서포트하고 있습니다. 즉, DynamoDB 에 데이터가 생성/수정/삭제되면, DynamoDB Streams 를 통해 해당 데이터조작 이력을 다른 데이터소스에 밀어넣을수 있다는 것이죠! Eventual Consistency (최종일관성) 라 불리는 기법이기도 한 이 접근법을 활용하면, DynamoDB 로 입력된 데이터를 Elasticsearch 에 밀어넣어서 DynamoDB 와 Elasticsearch 의 데이터 싱크를 맞춰서, 간단한 쿼리 및 데이터 조작은 DynamoDB 로, 복잡한 쿼리는 Elasticsearch 로 처리하게 하여 매우 다양한 유즈케이스를 손쉽게 해결할 수 있습니다.

참고로, 아래 포스팅을 보시면 DynamoDB Streams + AWS Lambda 를 통해 어떻게 Elasticsearch 에 데이터를 동기화시킬 수 있는지에 대해 이해하실 수 있습니다.

본론으로 돌아와서, 다시 한번 DynamoDB 의 제약사항들에 대해 짚어보려고 합니다.

결론부터 말하면 DynamoDB 는 count() 를 지원하지 않고, 커서 기반의 페이지네이션만을 제공하므로 페이지 넘버를 지정하는 등의 오프셋 기반의 페이지네이션은 불가능합니다. 이게 무엇을 의미할까요? 다음과 같은 상황을 생각해봅시다.

Facebook 이나 Twitter 같은 앱의 타임라인에 올라온 포스팅을 보기위해 스크롤을 쭉 내리면, 바닥을 치기전에 새로운 포스팅들을 추가로 로딩해줍니다. 또 밑으로 스크롤을 계속 내리면 제일 마지막 포스팅을 기준으로 또 새로운 포스팅들을 추가로 로딩해줍니다. 즉, 한 페이지의 단위를 포스팅 20개로 가정하면, 최신순으로 정렬된 상태에서 90번째로 오래된 포스팅을 보기 위해서는 반드시 그 앞의 2, 3, 4 페이지를 거쳐 5페이지의 90번째 포스팅을 확인할 수 있다는 말이 됩니다. 이러한 유즈케이스는 앱에서는 지극히 당연하므로 별 문제될 것이 없지만, 이러한 애플리케이션의 관리자용 CMS 등에서 아래 이미지와 같이 페이지 상태바를 넣어야하는 요구사항을 떠올려봅시다.

이러한 경우에, DynamoDB 는 count() 기능이 없으므로 애초에 몇번째 페이지가 마지막 페이지인지 알 수 있는 방법이 없습니다. 또한, 각 페이지를 눌렀을 때 앞의 페이지를 모두 한번씩 차례대로 호출해 준 다음에서야 원하는 페이지로 이동할 수 있기때문에 극단적인 경우를 가정해서 첫번째 페이지에서 999번째 페이지로 이동하고 싶은 경우, 심각한 퍼포먼스 문제가 발생할 수 있습니다. (당연히 코드복잡도도 높아지고, 금전적으로 코스트도 상당히 높아지게 됩니다)

즉, 위와 같은 요구사항이 있는 경우에는 DynamoDB 로 열심히 어떻게든 해보려고 노력하는 것보다는 Elasticsearch 를 통해 해결하는 것이 훨씬 쉽고 효과적이라는 점을 떠올릴 수 있게됩니다.

처음에 말한대로, Amplify 의 List API 는 DynamoDB 를, Search API 는 Elasticsearch 를 데이터소스로 활용하고 있기 때문에, 관리자용 CMS 서비스를 개발할 때에 페이지 상태바 등을 넣어야한다면 List API 가 아닌 Search API 를 사용하는 것이 옳은 판단일 수 있습니다. 또한, 리스트를 대상으로 각 필드별 검색조건을 달아서 검색해야하는 경우, 각 필드별 정렬기능을 활용하고 싶을 경우 등에도 List API 가 아닌 Search API 를 사용하는 것이 좋은 경우가 많다고 생각합니다. 적절하게 List API 와 Search API 를 혼용해서 사용해도 물론 상관없지만, 제 경우에는 결국 코드 복잡도를 줄이기 위해서 Search 로 List 를 대신하게 되는 경우도 빈번하게 있었습니다.

반면, 위와 같은 요구사항이 없고 매우 단순한 형태의 리스트 기능만 사용하고 싶을 경우에는 당연히 List API 만을 사용해도 좋을 것 같습니다 :)

Search API 에 대하여

Elasticsearch 의 스펙 자체는 size 와 from 옵션을 통해 원하는 페이지로 한번에 이동할 수 있는 쿼리를 날릴 수 있도록 지원해주고 있지만, Amplify 에서 자동으로 생성해주는 resolver 및 API 라이브러리 코드에서는 DynamoDB 를 활용한 API 와의 일관성을 위해서인지, 토큰 기반의 페이지네이션 형태로 쿼리를 날리는 인터페이스를 제공해주고 있습니다. 쿼리의 결과로 다른 점이 있다면, DynamoDB 를 데이터소스로 하는 쿼리에서는 items 필드와 함께 nextToken 만 알려주는 반면에, Elasticsearch 를 데이터소스로 하는 쿼리는 이것들에 추가적으로 count 필드도 함께 제공해줍니다. 즉, 쿼리 대상이 되는 데이터의 전체 숫자를 알 수 있다는 것이죠! 이것만 알아도 관리자용 CMS 의 페이지 상태바의 마지막 페이지 번호가 무엇인지는 쉽게 계산할 수 있습니다.

물론, 해당 페이지로 직접 접근할 수 있는 방식이 아니므로 여러번 호출해야 원하는 페이지에 도달할 수 있다는 치명적인 단점은 해결되지 않으나, Amplify 에서 기본적으로 생성해주는 resolver 가 아닌 custom resolver 를 작성함으로써 이러한 문제를 해결할 수도 있습니다. custom resolver 는 Amplify 로 개발함에 있어서 활용가치가 매우 높은 기능이므로 아래 글을 통해 반드시 이해하고 적재적소에 활용해보는 것을 적극 권장합니다!!

참고로, 내가 사용하려고 하는 API 의 데이터 소스가 DynamoDB 인지, Elasticsearch 인지 구분하는 방법은 API 이름의 시작이 Search 로 시작하는지 여부로 바로 알 수 있습니다. Search 로 시작하는 API 만이 Elasticsearch 데이터 소스를 사용하게 되며, Create, Get, Update, Delete, List 등으로 시작하는 API 는 전부 DynamoDB 를 데이터소스로 사용하는 API 입니다 :)

@searchable 에 대하여

본 글의 타겟층은 Amplify 입문자분들이 아닌, 어느정도 Amplify 의 튜토리얼을 마치고 실제로 프로젝트에 Amplify 를 활용하고자 하시는 분들을 대상으로 하고 있기에 AWS Amplify 의 기초적인 것들은 설명하지 않겠지만, 이 글을 읽는 분들 이라면 모두 아시다시피 Search API 를 사용하기 위해서 필요한 것은 GraphQL 스키마의 type 선언 바로 옆에 @searchable 디렉티브를 적어두는 것밖에 없습니다.

그런데, 공식 문서를 잘 읽어보시면 몇가지 주의사항들이 있습니다.

AWS Amplify @searchable 사용시의 주의사항에 대하여

Note: @searchable is not compatible with DataStore but you can use it with the API category.

Note: @searchable is not compatible with Amazon ElasticSearch t2.micro instance as it only works with ElasticSearch version 1.5 and 2.3 and Amplify CLI only supports instances with ElasticSearch version >= 6.x.

Note: Support for adding the @searchable directive does not yet provide automatic indexing for any existing data to Elasticsearch. View the feature request here.

Migration warning: You might observe duplicate records on search operations, if you deployed your GraphQL schema using CLI version older than 4.14.1 and have thereafter updated your schema & deployed the changes with a CLI version between 4.14.1 - 4.16.1. Please use this Python script to remove the duplicate records from your Elasticsearch cluster. This script indexes data from your DynamoDB Table to your Elasticsearch Cluster. View an example of how to call the script with the following parameters here.

첫번째로, DataStore 를 사용할 때는 @searchable 을 활용할 수 없다는 점.

두번째로, Amazon Elasticsearch 의 t2.micro 인스턴스를 사용할 수 없다는 점.

세번째로, DynamoDB 를 데이터소스로 연계해둔 기존의 type 에 @searchable 을 붙이더라도, 기존에 DynamoDB 에 저장되어 있던 데이터들에 대해서는 Elasticsearch 로 동기화 되지 않아서 "아직은" 인덱싱이 되지 않는 다는 점. (이 경우에는 직접 기존 데이터를 마이그레이션 해주면 해결될 수 있긴 할 것 같습니다)

네번째로, CLI 버전문제로 데이터가 중복저장 될 수 있다는 점.

그 어떤 점들도 그냥 가볍게 지나치기 어려운 특징들이니 꼼꼼히 살펴보는 것이 좋겠죠?!

마치며

이번 글에서는 List API 와 Search API 가 가진 특성을 이해하고, 어떤 상황에서 어떤 API 를 써야할지에 대한 판단 기준에 대해 설명했습니다.

이상, 컨설팅부의 김태우였습니다! :D