AWS 기술 블로그

디프로모션의 DynamoDB, zero-ETL, ElastiCache Serverless 도입기

서비스의 간단한 소개 및 특징

브랜드의 중요한 순간을 함께 만들어가는 파트너. 디프로모션은 누구나 쉽고 빠르게 디지털 캠페인과 프로모션을 만들 수 있도록 돕는 서비스입니다. 직관적인 온라인 디자인 에디터를 통해 룰렛, 스크래치, 틀린그림찾기, 퀴즈, 래플 등 다양한 유형의 이벤트를 손쉽게 생성할 수 있으며, 당첨자 선정과 리워드 지급까지 하나의 플랫폼에서 관리하여 브랜드가 목표로 하는 마케팅 결과를 효과적으로 달성할 수 있도록 지원합니다.

2023년 정식 서비스 출시 이후, 디프로모션은 천만 건 이상의 브랜드와 소비자 간 상호작용을 이끌어냈습니다. 다양한 산업군의 기업들과 협력하며 전년 대비 8배 성장이라는 성과를 기록한 동시에, 가파른 성장 과정에서 나타난 다양한 요구 사항들을 충족하기 위해 기술적 개선을 이어가고 있습니다.

이번 글에서는 HAQM DynamoDB를 비롯한 Zero-ETL, HAQM ElastiCache Serverless까지 적용한 현재의 시스템으로 발전하기까지의 과정과, 그 여정에서 얻은 교훈을 나눕니다. 디프로모션이 어떻게 더 나은 서비스를 만들어가고 있는지, 그 이야기를 지금부터 함께 살펴보세요.

서비스 런칭 시기의 초기 구조

서비스 런칭을 준비하면서 저희는 가장 이상적인 기술보다는 현실적으로 가장 적절한 선택이 무엇일지 고민했습니다. 스타트업으로서 자원과 인원이 여유롭지 않은 상황에서, 캠페인 서비스라는 특성상 트래픽이 예상보다 적을 수도, 때로는 예측하기 어려운 수준으로 급증할 수도 있었기에, 우리는 안정성과 확장성은 물론 비용 효율성까지 모두 고려해 균형 잡힌 데이터베이스 구조를 찾아야 했습니다.

프로모션 서비스에서 다루는 데이터들은 서로 다른 특성과 요구사항을 가지고 있습니다. 사용자의 이벤트 참여는 최소한의 지연시간으로 처리해야 했고, 페이지 디자인 설정이나 메타데이터, 미디어 파일 같은 정적 리소스는 효율적인 전송과 관리가 필요했습니다. 또한, 프로모션별로 다양한 구조의 설정값과 참여자 데이터가 존재하기 때문에, 이를 유연하게 저장하고 검색할 수 있는 방식이 필요했습니다.

일반적으로 관계형 데이터베이스(RDBMS)를 고려할 수도 있었지만, 프로모션의 데이터 구조는 정형화되지 않은 경우가 많고, 트래픽 패턴이 예측하기 어려운 특성을 가졌습니다. 특정 이벤트 기간 동안 트래픽이 급증하거나, 프로모션마다 수집하는 데이터 형태가 다를 수 있기 때문에, 스키마 유연성과 탄력적 확장이 가능한 NoSQL 기반의 구조가 더 적합하다고 판단하여 다음과 같이 목적별로 특화된 조합을 선택했습니다.

MemoryDB for Redis: 서비스의 데이터 허브

서비스 전반의 데이터 처리를 위한 핵심 데이터베이스로 MemoryDB for Redis를 선택했습니다. 인메모리 데이터베이스의 특성상 높은 처리 성능을 제공하면서도, 클러스터 노드 추가와 삭제만으로 손쉽게 수평 확장과 축소가 모두 가능했기 때문입니다. 특히 데이터 영속성을 지원하면서도 Redis의 다양한 자료구조를 활용할 수 있어, 서비스의 데이터 처리 허브로서 이상적이라고 판단했습니다.

HAQM OpenSearch: 유연한 데이터 검색과 분석

프로모션마다 다르게 설계되는 데이터 구조를 효율적으로 다루기 위해 OpenSearch를 도입했습니다. Document 기반의 유연한 스키마는 다양한 형태의 데이터를 저장하고 검색하는 데 적합했으며, 분산 아키텍처를 통한 안정적인 확장도 가능했습니다.

HAQM S3 + HAQM CloudFront: 정적 리소스의 효율적인 관리

미디어 리소스, 페이지 구성, 메타데이터와 같은 정적 리소스는 S3에 저장하고 CloudFront를 통해 제공했습니다. S3의 높은 내구성과 무한한 확장성, CloudFront의 글로벌 캐싱을 통해 규모에 관계없이 정적 리소스를 안정적이고 효율적으로 전송할 수 있었습니다.

서비스 성장 속에서 마주한 기존 구조의 한계와 새로운 선택의 필요성

초기 구조는 서비스 런칭과 초기 성장 단계에서 안정적으로 작동했습니다. 하지만 서비스가 성장하면서 프로모션의 규모와 복잡도가 증가했고, 몇 가지 한계점들을 마주하게 되었습니다.

MemoryDB for Redis는 서비스의 데이터 처리 허브로서 잘 작동했지만, 프로모션 특성상 트래픽이 불규칙한 상황에서 효율적인 운영에 어려움이 있었습니다. 피크 타임의 트래픽을 고려해 클러스터를 유지해야 했기에 자원이 충분히 활용되지 못하는 시간대가 많았고, 데이터 쓰기 작업이 늘어날수록 운영 비용도 생각보다 가파르게 증가했습니다.

OpenSearch도 비슷한 고민이 있었습니다. 프로모션의 종류가 다양해지고 수집하는 데이터의 구조가 복잡해질수록, 샤드 증설과 인덱스 최적화에 더 많은 리소스가 필요했습니다. 특히 대량의 데이터 적재가 필요한 상황에서는 예상보다 많은 비용이 발생했고, 특히, 인덱싱과 검색 요청이 동시에 증가하는 환경에서는 노드의 GC(Garbage Collection) 시간이 길어지고, 전체 클러스터 응답 속도가 저하되는 문제가 발생했습니다. 이러한 문제는 단순히 샤드를 추가하는 방식으로 해결되지 않으며, 클러스터 구성 자체의 한계를 넘어서는 트래픽에서는 즉시 조치가 어려운 경우도 있었습니다.

우리에게는 더 효율적이고 탄력적인 데이터베이스 솔루션이 필요했습니다. 특히 다음과 같은 요구사항들이 중요했습니다:

  • 실제 트래픽에 따라 자원이 자동으로 조절되는 탄력적인 확장성
  • 다양한 데이터 구조를 유연하게 저장하면서도 일관된 성능 보장
  • 최소한의 운영 부담으로 안정적인 서비스 제공
  • 데이터 처리량과 비용 간의 균형 잡힌 효율성

이러한 고민 끝에 우리는 HAQM DynamoDB 도입을 결정했습니다. DynamoDB는 완전 관리형 서비스로 운영 부담을 크게 줄일 수 있었고, 온디맨드 용량 모드를 통해 실제 사용량에 따른 정확한 리소스 조정이 가능했습니다. 무엇보다 일관된 밀리초 단위의 성능을 제공하면서도, 테이블 파티셔닝을 통해 데이터를 효율적으로 분산 저장할 수 있다는 점이 우리의 필요와 잘 맞았습니다.

DynamoDB 도입 이후의 성과, 그리고 다시 만난 한계

서버리스 NoSQL DB인 DynamoDB의 두 가지 모드는 각각 다른 특성을 가지고 있습니다.

프로비저닝 모드는 처리량을 사전에 정의하고 예약 구매도 가능해 단위 처리량당 비용이 저렴하지만 용량을 미리 설정해야 하고, 온디맨드 모드는 실제 사용량에 따라 자동으로 리소스가 조정되어 용량 계획에 대한 부담이 없고 트래픽 변동이 심한 워크로드에 유연하게 대응할 수 있지만 더 높은 비용이 발생합니다.

이러한 특성을 분석한 후, 우리는 사용자와의 즉각적인 상호작용이 필요하고 트래픽 예측이 어려운 데이터는 온디맨드 모드로, 상대적으로 일정한 패턴을 보이는 데이터는 프로비저닝 모드로 구분하여 운영 효율성과 비용 효율성의 균형을 맞추었습니다. 이 접근 방식으로 수동 리소스 관리가 필요 없어졌고, 초당 수백만 건의 요청도 안정적으로 처리할 수 있었으며, TTL을 활용한 데이터 수명 주기 관리와 테이블 통합으로 관리 부담을 줄이고 AWS 콘솔의 직관적 기능으로 운영 효율을 높일 수 있었습니다.

그러나 온디맨드 모드에서는 이전 피크 트래픽의 최대 2배 이내일 때만 자동 리소스 조정이 가능하고, 급격한 트래픽 증가에는 사전 테이블 워밍이 필요했습니다. 또한 최소한의 테이블 구성으로 싱글 테이블 디자인에 가까운 구조가 되었고, 다양한 조회 패턴 지원을 위한 Global Seconadry Index 사용은 추가 비용을 발생시켰으며, 복잡한 집계 쿼리나 조건 검색은 DynamoDB만으로 효율적 처리가 어려웠습니다. 이러한 한계를 극복하기 위해 우리는 기존 OpenSearch의 강점을 활용하는 방향으로 전환하여, DynamoDB의 뛰어난 쓰기 성능은 유지하되 집계와 검색 데이터는 OpenSearch에서 처리하도록 했습니다. DynamoDB Streams를 통해 데이터 변경 사항을 실시간으로 OpenSearch로 전송하는 zero-ETL 파이프라인을 구축했으며, 이 과정에서 DynamoDB Streams와 PITR 기능이 핵심적인 역할을 했습니다.

통합 구성 과정에서 여러 기술적 어려움이 있었지만, 단계별 검증과 테스트를 통해 최적화된 솔루션을 구축할 수 있었습니다. . 이어지는 내용에서는 이 과정에서 우리가 겪은 구체적인 경험과 해결 방법을 공유하고자 합니다.

DynamoDB와 OpenSearch간의 zero-ETL 파이프라인을 구축하며 알게된 것들

DynamoDB와 OpenSearch 간의 zero-ETL 파이프라인을 구축하기 위해서는 DynamoDB의 두 가지 핵심 기능이 필요합니다. 하나는 테이블의 데이터 변경 사항을 실시간으로 캡처하여 별도의 로그 스트림으로 제공하는 DynamoDB Streams이고, 다른 하나는 테이블 데이터를 특정 시점으로 복원할 수 있는 백업 및 복구 기능인 PITR(Point-In-Time Recovery)입니다. 이 두 기능의 조합으로 zero-ETL 파이프라인이 동작하게 되는데, 도입을 검토하면서 우리는 다음 세 가지 핵심 질문에 대한 답을 찾아야 했습니다.

  1. zero-ETL 파이프라인은 어떻게 기존 데이터를 OpenSearch에 기록하는가?
    파이프라인은 생성 시점에 PITR을 이용해 해당 시점까지의 데이터를 백업하고, 이를 OpenSearch에 저장합니다. 이후 DynamoDB Streams를 통해 기록되는 변경 사항들을 전송하여 실시간 동기화를 유지하는 방식으로 동작했습니다.
  2. 파이프라인에 오류가 발생하는 경우 데이터는 어떻게 처리되는가?
    파이프라인 생성 단계에서 오류가 발생하면 PITR 스냅샷 데이터의 동기화는 단 한 번만 시도되며, 실패할 경우 파이프라인을 다시 생성해야 했습니다. 파이프라인이 정상 생성된 후 발생하는 오류의 경우, DLQ(Dead-letter Queue)를 통해 처리할 수 있었습니다. Data Prepper 2.3부터는 S3를 DLQ로 지원하여, 오류가 발생한 데이터를 S3에 저장할 수 있었습니다. 다만 S3에 저장된 데이터의 자동 리드라이브 기능은 제공되지 않아, 필요한 경우 별도의 처리 로직을 구현해야 했습니다.
  3. 파이프라인 구축 전에 OpenSearch에 이미 데이터가 존재하는 경우 어떻게 되는가?
    파이프라인은 bulk index 요청을 통해 데이터를 기록하므로, OpenSearch 문서 ID가 동일한 경우 PITR 스냅샷 데이터로 덮어쓰기됩니다. 파이프라인 구성 시 문서 ID 생성 방식을 지정할 수 있는데, 일반적으로는 DynamoDB의 기본키를 활용하게 됩니다. 다른 방식으로 문서 ID를 지정하는 경우에는 이 점을 특히 유의해야 했습니다.

이러한 검증 과정을 거쳐 우리는 zero-ETL 파이프라인 도입을 결정했습니다. 실시간에 가까운 동기화가 가능하고, 문제 발생 시 PITR 스냅샷을 통한 전체 데이터 재동기화가 가능하다는 점이 특히 매력적이었습니다.

현재는 DynamoDB의 테이블 구조와 OpenSearch의 인덱스 구조가 다른 상황에서도 유연하게 대응할 수 있도록, 데이터 변환과 매핑 템플릿을 정의하여 하나의 테이블에서 여러 인덱스로 데이터가 분산되도록 구성하여 운영하고 있습니다.

To-Be Architecture

더 나은 성능과 효율성을 위해 찾은 추가 퍼즐, ElastiCache Serverless 도입

DynamoDB와 OpenSearch의 zero-ETL 파이프라인 구축은 데이터 저장과 분석에서 상당한 개선을 가져왔지만, 여전히 서비스에서 중요한 요소인 응답 속도와 실시간 처리 성능 면에서 몇 가지 도전 과제가 남아 있었습니다. 특히, 자주 조회되는 데이터의 경우 매번 데이터베이스에서 직접 조회할 때 발생하는 레이턴시를 최소화하는 것이 필요했습니다. 이에 우리는 캐시 계층을 추가하는 방안을 검토하게 되었고, AWS ElastiCache Serverless를 선택하여 기존 인프라의 성능과 효율성을 한 단계 더 끌어올릴 수 있었습니다.

왜 ElastiCache Serverless인가?

ElastiCache Serverless는 필요할 때만 리소스를 프로비저닝하여 과도한 비용 없이 고성능 캐싱 기능을 제공할 수 있다는 점에서 우리의 요구 사항에 딱 맞는 솔루션이었습니다. 특히 다음과 같은 이유에서 Valkey 엔진을 선택했습니다:

  1. 자동 확장성
    프로모션 서비스 특성상 특정 시간대나 이벤트에 집중되는 트래픽 급증이 잦았습니다. ElastiCache Serverless는 요청량에 따라 자동으로 확장 및 축소되기 때문에, 피크 트래픽에서도 성능이 안정적으로 유지될 수 있었습니다.
  2. 낮은 운영 부담
    기존의 MemoryDB는 고성능 캐시로 적합했지만, 클러스터 구성과 확장 관리에 있어 운영 부담이 있었습니다. 반면 ElastiCache Serverless는 관리형 서비스로, 복잡한 클러스터 설정 없이 캐싱 계층을 운영할 수 있었습니다.
  3. Valkey 엔진의 데이터 모델 유연성
    Valkey 엔진은 Key-Value 기반으로 빠른 읽기/쓰기 성능을 제공하며, 서비스의 주요 데이터(예: 자주 조회되는 프로모션 설정, 리워드 정보)를 효율적으로 캐싱하는 데 적합했습니다.
  4. 비용 효율성
    Serverless 방식은 사용한 만큼만 비용이 발생하기 때문에, 트래픽이 낮은 시간대에는 불필요한 리소스 비용이 절감되었습니다.

도입 과정과 구현

ElastiCache Serverless 도입 과정에서 우리는 다음 단계를 거쳤습니다:

  1. 주요 캐시 데이터 정의
    먼저 서비스에서 가장 자주 조회되며 실시간으로 제공해야 하는 데이터를 식별했습니다.
  2. DynamoDB와의 연동
    DynamoDB에서 읽기 빈도가 높은 데이터를 ElastiCache Serverless에 캐싱하고, 데이터 업데이트 시 TTL(Time-To-Live)을 활용해 캐시를 갱신하는 구조를 설계했습니다.
  3. 캐싱 전략 수립
    데이터를 읽는 패턴에 따라 다음과 같은 캐싱 전략을 적용했습니다:

    • Write-Through Cache: DynamoDB에 쓰기 작업이 발생하면 캐시도 즉시 갱신하도록 설정.
    • Lazy Loading: 캐시에 없는 데이터에 대한 요청이 있을 때만 DynamoDB에서 조회 후 캐시에 저장.
    • TTL을 설정하여 오래된 데이터를 자동으로 제거하고, 항상 최신 상태를 유지.
  4. 성능 테스트 및 최적화
    도입 후 대규모 트래픽 시뮬레이션을 통해 ElastiCache Serverless의 성능을 검증했습니다. 트래픽 증가 시에도 응답 시간이 평균적으로 06~40ms로 유지되었으며, 이전보다 최대 50% 이상의 읽기 작업이 DynamoDB에서 ElastiCache로 분산되었습니다.

코드 적용

데이터의 성격에 따라 캐싱 전략을 사용하고 있습니다. 먼저 데이터가 변경될 여지가 적거나, 요청량이 많을 것으로 예상되는 데이터에는 Write-Through 방식을 사용하여 첫 요청부터 cache가 hit 될 수 있도록 구성하였습니다.

// PromotionService.kt

// as-is
fun savePromotionConfig(command: SavePromotionConfigCommand): Mono<PromotionConfig> =
    this.promotionRepository.save(command)

// to-be
fun savePromotionConfig(command: SavePromotionConfigCommand): Mono<PromotionConfig> {
    val cacheKey = "promotion:config:${command.appKey}"
    val ttl = Duration.between(OffsetDateTime.now(), command.endDate) // 프로모션 종료일까지 TTL 설정
    return this.cacheRepository.cacheUntil(cacheKey, this.objectMapper.writeValueAsString(command), ttl)
        .flatMap {
            this.promotionRepository.save(command)
                .onErrorResume { ex -> // DynamoDB 저장 실패 시 캐시 삭제
                    logger.warn("Promotion save failed, rollback cache. appKey: ${command.appKey}", ex)
                    this.cacheRepository.delete(cacheKey)
                        .then(Mono.error(ex))
                }
        }
}

// cacheRepository.kt

// to-be
fun cacheUntil(key: String, value: String, ttl: Duration): Mono<Boolean> {
    if (duration.isNegative || duration.isZero) {
        return Mono.just(false)
    }

    return this.reactiveRedisOperations.set(key, value, ttl)
}

fun delete(key: String): Mono<Boolean> = 
    this.reactiveRedisOperations.delete(key)

더불어 데이터가 자주 변경되거나 주기적으로 최신 상태를 유지할 필요가 있는 경우에는 TTL 값을 요건에 맞게 세팅하고, cache miss가 발생했을 때 DyanmoDB에서 데이터를 조회한 후 캐시에 저장하는 Look-Aside 방식으로 구성했습니다. 사실 앞선 Write-Through 방식에서도 캐시 저장에 실패할 때를 대비해 조회 시에는 모두 Look-Aside 방식으로 조회하고 있습니다.

// PromotionService.kt

// as-is
fun getPlatformOauthToken(appKey: String): Mono<PlatformOauthToken> =
    this.platformRepository.get(appKey)

// to-be
fun getPlatformOauthToken(appKey: String): Mono<PlatformOauthToken> {
    val cacheKey = "platform:oauth:${appKey}"
    return this.cacheRepository.get(cacheKey)
        .switchIfEmpty {
            this.platformRepository.get(appKey)
                .delayUntil { platformOauthToken -> // 캐시 저장
                    this.cacheRepository.cacheUntil(
                        cacheKey,
                        this.objectMapper.writeValueAsString(platformOauthToken),
                        Duration.ofMinutes(20)
                    )
                }
        }

ElastiCache Serverless 도입 이후 얻은 효과

ElastiCache Serverless를 도입한 결과, 서비스 전반에서 다음과 같은 개선 효과를 얻을 수 있었습니다:

  1. 레이턴시 감소
    자주 조회되는 데이터에 대해 평균 응답 시간이 최대 87% 이상 단축되었습니다. 특히 대규모 트래픽 상황에서도 사용자 경험이 안정적으로 유지되었습니다.
  2. DynamoDB 비용 절감
    읽기 요청의 상당 부분이 ElastiCache Serverless로 이동하면서 DynamoDB의 읽기 용량 비용이 대폭 감소했습니다.
  3. 운영 효율성 향상
    Serverless 방식 덕분에 트래픽 패턴에 따른 리소스 조정이 자동화되어 운영팀의 부담이 줄었습니다.
  4. 확장 가능성 확보
    ElastiCache Serverless는 앞으로 데이터 규모와 트래픽이 더 증가하더라도 추가적인 인프라 변경 없이 확장이 가능합니다.

도입 전 DynamoDB to ALB Latency

도입 후 Valkey ALB Latency

마무리: 지속적인 최적화를 향해

MemoryDB to DynamoDB, OpenSearch Zero-ETL, 그리고 ElastiCache Serverless 도입까지, 각 단계를 거치는 과정에서 얻은 가장 중요한 성과는 단순한 성능 개선이나 비용 절감 이상의 것이었습니다. 완벽한 단일 솔루션은 존재하지 않으며, 우리가 직면한 문제와 서비스 특성에 맞춰 최적의 조합을 지속적으로 찾아가야 한다는 점을 직접 경험하고 검증하는 과정이었습니다.

이 과정에서 얻은 실질적인 효과는 다음과 같습니다.

  • 트래픽 변동성에 대한 유연한 대응과 운영 비용 효율화
  • 실시간 데이터 동기화와 분석 역량 강화
  • 응답 시간 단축을 통한 더 나은 사용자 경험 제공

그러나 현재에 만족하거나 확신하기보다는, 더 나은 서비스를 위해 지속적으로 개선해 나가야 한다는 점 또한 분명해졌습니다. 앞으로 해결해야 할 과제는 다음과 같습니다.

  • 캐시 활용 범위 확장 및 효율적인 캐싱 전략 연구
  • NoSQL의 쿼리 성능 및 저장 최적화
  • TTL 정책 정교화
  • 트래픽 패턴 분석 자동화 파이프라인 구축을 통한 운영 최적화

서비스가 성장하면서 점점 더 다양한 요구사항과 새로운 기술적 도전이 나타날 것입니다. 하지만 지금까지 배운 중요한 교훈을 바탕으로, 단순한 기술 스택의 변경이 아니라, 사용자 경험을 더욱 향상시키는 방향으로 서비스와 함께 유연하게 진화할 수 있는 구조를 고민하고 지속적으로 개선해 나갈 예정입니다.

앞으로 디프로모션이 어떻게 성장해 나가는지 함께 지켜봐 주세요.

EunsikKim

김은식

김은식님은 디프로모션의 창업자이며, 서비스의 개선과 확장을 위해 새로운 시스템 아키텍처를 설계하고 제안하는 역할에 주력하고 있습니다.

ingonekim

김인곤

김인곤님은 디프로모션에서 서비스를 안정적으로 운영하고 배포할 수 있도록 백엔드 개발을담당하고 있습니다.

JunghwanJang

장정환

장정환님은 디프로모션이 성공적인 서비스로 자리잡을 수 있도록 백엔드 분야에서 아키텍처 설계, 개발, 리팩토링 등 전반적인 업무를 이끌고 있으며, 높은 트래픽 속에서 안정적인 사용자 경험을 위해 노력하고 있습니다.

Seunguk Kang

Seunguk Kang

강승욱 Solutions Architect는 다년간의 백엔드 개발의 경험을 바탕으로 AWS기반의 워크로드들을 스타트업에서 부터 현재는 Telco회사들과 함께 여정을 함께하고 있습니다.