AWS 기술 블로그
채널코퍼레이션의 Amazon DynamoDB와 함께한 아키텍처 현대화 여정 – 2부
채널코퍼레이션은 올인원 AI 메신저 ‘채널톡’을 운영하는 B2B SaaS 스타트업으로 Amazon DynamoDB의 수평 확장성, ACID 트랜잭션과 같은 특징을 활용해 빠르게 성장하는 비즈니스를 문제없이 수행하고 있습니다. 하지만 key-value 데이터베이스인 DynamoDB의 특성으로 인해 몇몇 문제는 DynamoDB 이외의 다른 서비스와 결합해야 쉽게 해결 할 수 있었습니다. 지난 블로그 1부에서 채널코페레이션이 비즈니스 성장과 함께 겪었던 기술적 문제들, NoSQL 도입을 위한 동기, 그리고 DynamoDB와 함께 문제를 해결했던 여정을 설명했다면, 2부에서는 DynamoDB 만으로 해결 할 수 없었던 영역을 다른 AWS 서비스와의 통합을 통해 얻은 경험을 공유합니다.
정형 및 비정형 데이터 검색 문제
DynamoDB를 위한 NoSQL 설계를 인용해보면 “DynamoDB와 같은 NoSQL 데이터베이스에서는 몇 가지 방법으로 데이터를 효율적으로 쿼리 할 수 있지만, 그 외에는 쿼리 비용이 높고 속도가 느립니다”.
이런 특성을 보았을때 다음과 같은 채널코퍼레이션의 사용 사례는 DynamoDB 만으로 해당 문제를 풀기 어려웠습니다.
- 필터링 조건이 다양한 정형 데이터 검색 – 메시지 검색
-
- 채널톡은 상담 검색을 통해서 대화 내용을 빠르게 대화 내용을 검색 할 수 있습니다. 담당자, 팀, 팔로워 등 다양한 필터를 추가해서 검색할 수 있어야 합니다.
- 비정형 데이터 검색 – 고객 데이터 검색
-
- 고객 데이터는 채널코퍼레이션 고객이 고객의 어떤 정보를 넣을지에 따라서 데이터의 스키마(Schema)가 달라집니다. 이러한 스키마가 정해져 있지 않고 현재 기준으로 최대 100만명의 고객의 데이터를 여러 필드를 활용해 빠르게 검색 할 수 있어야 한다는 특징이 있습니다.
위와 같은 문제들은 DynamoDB보다 Amazon OpenSearch Service 등과 같이 검색을 위한 별도 서비스를 활용하는 것이 더 효과적이고 효율적이라고 판단하였습니다. 이를 위해서는 DynamoDB와 다른 서비스 간 데이터 동기화를 해야 합니다.
스트림 소개
시간의 순서에 따라 정렬된 일련의 연속적인 이벤트를 스트림이라고 합니다. DynamoDB는 변경된 데이터(CDC: change data capture)를 스트림(Stream)으로 사용 할 수 있게 두 가지 방법을 제공합니다.
- DynamoDB Streams는 DynamoDB 테이블에서 시간 순서에 따라 항목(Item) 수준 수정을 캡처하고 이 정보를 최대 24시간 동안 로그에 저장합니다.
- Amazon Kinesis Data Streams는 모든 DynamoDB 테이블에서 항목 수준 수정 사항을 캡처하여 Kinesis Data Streams에 복제합니다. 더 긴 데이터 보존 시간을 활용할 수 있으며, 향상된 팬아웃(fan-out) 기능을 통해 두 개 이상의 다운스트림 애플리케이션에서 동시에 액세스 할 수 있다는 장점이 있습니다.
이를 이용하면 위에 언급한 메시지와 고객 데이터 검색은 DynamoDB의 변경 데이터를 스트림을 이용해 검색을 잘 할 수 있는 서비스로 전달하여 문제를 쉽게 해결 할 수 있습니다. 두 방법의 특징과 유의점은 다음과 같습니다.
DynamoDB Streams
특징
- AWS Lambda와 함께 사용 시 DynamoDB Streams의 비용은 무료
- 시간 순서에 따라 데이터 전달 가능
유의점
- 스트림 처리 계층인 Lambda에서 장애 발생시 처리 필요
- Starting position을 LATEST로 하는 경우 배포시 누락 가능성 존재
Kinesis Data Streams
특징
- Kinesis Data Streams과 Lambda 비용이 각각 발생
- 시간 순서에 따라 전달하는 것을 보장하진 않음
- 하루 이상 데이터를 저장하는 것이 가능
- Lambda를 특정 시점부터 시작(Starting position)하게 만드는 것이 가능
유의점
- 스트림 처리 계층인 Lambda에서 장애 발생 시 처리 필요
- Starting position을 LATEST로 하는 경우 배포시 누락 가능성 존재
- 이벤트가 시간 순서대로 유입되지 않아 역순 이벤트 발생 가능
- 중복 이벤트 발생 가능
이를 자세히 이해하기 위해서 DynamoDB에서 각 스트림으로 어떻게 데이터가 전달되고 전달된 스트림에서 어떻게 Lambda에서 수행이 되는지 살펴보겠습니다.
DynamoDB Streams
DynamoDB Streams를 사용하게 되면 DynamoDB 파티션에서 하나의 샤드(shard)로 단일 항목 수정 내역(Stream Record)들을 보냅니다. 이로 인하여 각 파티션 내의 항목 레벨 변경사항들은 항상 순서가 보장됩니다.
Kinesis Data Streams
Kinesis Data Streams의 데이터 레코드(Data record)는 항목 변경 사항이 발생했을 때와 다른 순서로 표시될 수 있고, 동일한 항목 알림이 스트림에 두 번 이상 표시될 수도 있습니다. ApproximateCreationDateTime 속성을 확인하면 대략적인 항목 수정이 발생한 순서를 식별하고 중복 레코드를 식별할 수 있습니다.
이벤트 소스 매핑(Event source mapping)은 스트림 및 대기열 기반 서비스에서 항목을 읽고 레코드 배치로 함수를 간접적으로 호출하는 Lambda 리소스로, 이벤트 소스 매핑을 사용해 Lambda 함수를 직접 호출하지 않는 서비스의 스트림 또는 대기열에서 항목을 처리할 수 있습니다. 즉 Lambda가 바로 Streams에서 호출되는 것이 아닌 해당 리소스를 통해서 호출이 된다는 사실을 이해하고 다시 위의 특성과 문제를 접근해 보겠습니다.
DynamoDB Streams
Lambda에서 장애 발생 시 처리
이벤트 소스 매핑 구성 파라메터의 MaximumRecordAgeInSeconds 와 MaximumRetryAttempts 값 설정을 통해 기본적인 재시도 처리는 할 수 있습니다. 하지만 Lambda 코드에 버그가 생기거나 배포시 실수 하는 등 다양한 이유에서 재시도 만으로 해결 할 수 없는 장애는 발생 할 수 있습니다.
이벤트 소스 매핑 리소스를 살펴보면 On-failure destination 설정을 통해서 처리 할 수 없는 레코드에 대한 알림을 Amazon Simple Queue Service(Amazon SQS) 혹은 Amazon Simple Notification Service(Amazon SNS)로 전달하는 것이 가능 합니다.
이를 이용해 처리하지 못한 레코드를 재시도 할 수 있는데, 이를 위한 수신 메시지 예제는 다음과 같습니다.
위 정보를 토대로 DynamoDB Streams에서 해당 레코드를 검색해서 다시 시도해야 합니다.
이처럼 DynamoDB Streams에서 레코드를 다시 검색해서 처리하는 경우엔 모든 이벤트가 시간 순서대로 전달되지 않아 역순 이벤트가 발생 할 수 있게 됩니다.
만약 이벤트 소스 매핑의 BatchSize가 1보다 크다고 가정하면 Lambda 함수도 실행 중 다양한 원인으로 인해 일부 아이템 처리가 실패를 하는 경우가 발생할 수 있습니다. Lambda 함수가 재시도를 하게 되고 배치(Batch)로 기존에 처리한 레코드들이 동일하게 다시 들어오게 됩니다. 이런 경우에 대해 미리 별도 처리가 되어 있지 않는다면 중복 이벤트 발생 가능성도 생길 수 있습니다.
또한 이벤트 소스 매핑의 Starting position을 LATEST로 하는 경우 이벤트를 놓칠 수 있습니다.
MaximumRetryAttempts, MaximumRecordAgeInSeconds와 같은 이벤트 소스 매핑에 설정된 값들은 처음 설정과 달리 에러 처리 및 상황에 따라서 변경을 해야하는 경우가 있습니다. 이때 의도치 않게 일부 레코드를 놓칠 수 있게 됩니다.
이를 해결하기 위해 Starting position을 TRIM_HORIZON으로 변경 시 DynamoDB Streams에 있는 모든 데이터가 처음부터 이벤트 소비자에게 전달 됨으로 역순 이벤트와 중복 이벤트가 발생 할 수 있게 됩니다.
문제 정리
결국 DynamoDB Streams와 Kinesis Data Streams 모두 비슷한 문제를 해결해야 됨을 알 수 있습니다.
정리해 보면 아래와 같이 두 가지 케이스로 바꿔서 이야기 해 볼 수 있습니다.
- 모든 스트림 처리를 멱등성 있게 함수 작성이 가능한가?
- Lambda에서 문제가 발생 시 재시도 할 수 있는가?
멱등성
모든 스트림 처리에서 가장 중요시 되고 문제없이 되어야하는 것은 멱등성있게 로직을 작성하는 것 입니다. 이벤트 소비자에서 이렇게만 작성 되어도 많은 문제가 해결됩니다.
예를 들면 시간 순서대로 들어오지 않아 역순 이벤트가 발생하는 상황을 보겠습니다.
위의 그림과 같이 역순 이벤트로 인해 데이터 정합성이 깨질 수 있게 됩니다.
이를 해결 하기 위해서는 모든 Create, Update, Delete의 상황에서 발생하는 이벤트들이 시간의 순서대로 수행이 된다는 것을 보장이 된다면 하나의 상태는 모두 최종 상태가 동일하기 때문에 문제가 생기지 않을 것 입니다.
즉 마지막 현재 상태가 가장 최신의 이벤트에 의한 결과라는 것을 보장해 주면 위의 문제는 쉽게 해결 됩니다.
이를 위해 위의 케이스에서 현재 상태 이후 이벤트들만 수행하기 위해 시간을 나타내는 timestamp가 더 큰 경우에만 업데이트가 된다고 가정하고 다시 작성해 보겠습니다.
이 경우 역순 이벤트가 발생 하였지만 현재 상태보다 과거의 이벤트이므로 수행이 되지 않기 때문에 동일한 결과 값을 얻을 수 있습니다. DynamoDB에서는 version 번호를 이용한 낙관적 잠금이 가능한데 version은 항목을 업데이트할 때마다 버전 번호가 일정하게 자동으로 오릅니다. 업데이트 또는 삭제 요청은 클라이언트 측 객체 버전이 DynamoDB 테이블의 해당 항목 버전 번호와 일치 해야만 가능하게 됩니다. 이런 특성을 이용한다면 쉽게 문제를 해결 할 수 있습니다.
단 위와 같은 로직으로 수행이 된다면 Create, Update에 대해서는 보장할 수 있지만 다음 그림의 오른쪽 예제처럼 Delete에 대한 문제가 되는 케이스가 존재 합니다.
이러한 경우에도 이벤트의 발생 순서를 보장하기 위해 서비스에서 레코드를 hard delete가 아닌 soft delete를 사용을 하면 문제를 해결할 수 있습니다. 예를 들면 아래에 A가 삭제 되었고 언제 삭제 되었는제 정보가 있다면 새로 생성하려고 하는 시점이 삭제된 시점 이후 이벤트로 판단해 생성이 되지 않게 만들 수 있습니다.
장애 발생 시 복구 방법
이제 멱등성있게 모든 로직이 작성되어 있다고 가정하고 문제 발생시 재시도를 할 수 있는지에 대해서 이야기 해보겠습니다.
Kinesis Data Streams와 DynamoDB Streams 모두 On-failure destination 설정이 가능하고 과거의 데이터를 스트림 소비자에게 다시 전달하는 것이 가능합니다. 하지만 두 스트림에 대한 전략이 다를 수 있습니다.
- DynamoDB Streams
- DynamoDB Streams는 이벤트 소스 매핑의 Starting position에서 LATEST와 TRIM_HORIZON를 제공합니다. 이로 인해 특정 시점의 레코드를 다시 얻기 위해서는 특정 샤드의 특정 Sequence Number부터 원하는 곳까지 읽고 다시 처리하기 위한 별도의 애플리케이션이 존재해야 해당 문제를 해결 할 수 있습니다.
- Kinesis Data Streams
- Kinesis Data Streams는 이벤트 소스 매핑의 Starting position에서 AT_TIMESTAMP를 포함한 다섯가지 옵션을 제공합니다. 이 특징은 문제가 생기는 시점 바로 이전으로 돌아가서, 이벤트 소스 매핑만 업데이트 하고 재배포 하면 문제를 해결 할 수 있게 됩니다.
채널코퍼레이션의 선택
DynamoDB가 제공하는 두 가지 스트림을 이용해 다른 서비스로 데이터 동기화 작업에서 생길 수 있는 케이스들을 알아 보았습니다. 두 스트림간의 장단점으로 인해 운영 시 고려해야 하는 부분과 비용적인 측면이 다르기 때문에 무조건 특정 스트림을 이용하는 것이 좋다고 말하기는 어렵습니다. 이에 채널코퍼레이션은 아래와 같은 기준으로 두 가지 스트림을 모두 사용하고 있습니다.
- DynamoDB Streams를 사용하는 경우
- 시간 순서대로 이벤트 발생이 중요한 경우
- 문제 발생 시 에러 복구 비용이 높아도 괜찮은 경우
- Kinesis Data Streams를 사용하는 경우
- 문제 발생 시 원하는 시점부터 빠른 복구가 중요한 경우
- 두 개 이상의 Lambda가 동시에 수행되어야 하는 케이스가 존재하는 경우
스트림을 이용한 온라인 테이블 마이그레이션 전략
스트림을 사용하는 또 하나의 예제로 채널코퍼레이션은 DynamoDB Streams를 이용해 온라인 테이블 마이그레이션 작업을 수행하고 있습니다. 동일한 방법을 활용하면 다른 AWS 계정 간에도 테이블을 마이그레이션 할 수 있게 됩니다.
Step 1
1. DynamoDB에서 새로운 스키마를 갖는 New table을 생성합니다.
2. Old table의 DynamoDB Streams 이벤트를 소비해 변경된 데이터를 New table 스키마로 변경해줄 Lambda 함수를 배포합니다.
Step 2
3. Lambda 함수가 배포되기 이전의 데이터를 읽어 New table로 스키마를 변경합니다.
Step 3
4. 새로운 API Server 를 배포합니다.
이 과정을 거치게 되면 스키마에 큰 변화가 있는 경우에도 라이브 마이그레이션이 가능해집니다. Step 2에서는 아래와 같이 New table로 데이터 입력하는 다양한 방법이 존재 할 수 있습니다.
- Amazon EMR을 이용
- AWS Glue를 이용
- 별도 애플리케이션을 이용
특정 시점의 데이터를 새로운 DynamoDB 테이블에 넣어야 할 때도 역시 멱등성으로 인해 고민해야 할 부분이 많이 있습니다. 이를 간소화 하기 위해서 채널코퍼레이션은 위와 같이 파이프 라인을 만들고 기존에 있는 모든 데이터에 대해서 version + 1을 위한 UpdateItem을 수행합니다. 이 경우 모든 아이템은 구성된 파이프라인을 따라 Lambda에서 마이그레이션을 수행하여 크게 신경쓰지 않고 New table로 데이터를 전달할 수 있습니다.
효과 및 결론
DynamoDB를 이용해 스케일링을 무한에 가깝게 할 수 있고, 다양한 다운스트림 서비스와의 종속성이 쉽게 제거 되었습니다.
특히 DynamoDB와 Kinesis Data Streams를 함께 활용해 애플리케이션 배포 중 문제가 발생해도 특정 시점부터 빠르게 복구가 가능해져 언제든 마음 편히 배포를 할 수 있게 되었습니다. 마지막으로 온라인 마이그레이션을 이용해 레거시를 쉽게 제거하고 효율적으로 테이블을 관리할 수 있습니다.