에반 • 피플팀에서 채용을 담당하고 있어요
채널코퍼레이션은 올인원 AI 메신저 ‘채널톡’을 운영하는 B2B SaaS 스타트업으로 Amazon DynamoDB의 수평 확장성, ACID 트랜잭션과 같은 특징을 활용해 빠르게 성장하는 비즈니스를 문제없이 수행하고 있습니다. 하지만 key-value 데이터베이스인 DynamoDB의 특성으로 인해 몇몇 문제는 DynamoDB 이외의 다른 서비스와 결합해야 쉽게 해결 할 수 있었습니다.
지난 블로그 1부에서 채널코페레이션이 비즈니스 성장과 함께 겪었던 기술적 문제들, NoSQL 도입을 위한 동기, 그리고 DynamoDB와 함께 문제를 해결했던 여정을 설명했다면, 2부에서는 DynamoDB 만으로 해결 할 수 없었던 영역을 다른 AWS 서비스와의 통합을 통해 얻은 경험을 공유합니다.
DynamoDB를 위한 NoSQL 설계를 인용해보면 “DynamoDB와 같은 NoSQL 데이터베이스에서는 몇 가지 방법으로 데이터를 효율적으로 쿼리 할 수 있지만, 그 외에는 쿼리 비용이 높고 속도가 느립니다”.이런 특성을 보았을때 다음과 같은 채널코퍼레이션의 사용 사례는 DynamoDB 만으로 해당 문제를 풀기 어려웠습니다.
필터링 조건이 다양한 정형 데이터 검색 – 메시지 검색
비정형 데이터 검색 – 고객 데이터 검색
위와 같은 문제들은 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에서 호출되는 것이 아닌 해당 리소스를 통해서 호출이 된다는 사실을 이해하고 다시 위의 특성과 문제를 접근해 보겠습니다.
이벤트 소스 매핑 구성 파라메터의 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를 함께 활용해 애플리케이션 배포 중 문제가 발생해도 특정 시점부터 빠르게 복구가 가능해져 언제든 마음 편히 배포를 할 수 있게 되었습니다. 마지막으로 온라인 마이그레이션을 이용해 레거시를 쉽게 제거하고 효율적으로 테이블을 관리할 수 있습니다.
We Make a Future Classic Product
채널팀과 함께 성장하고 싶은 분을 기다립니다