인게이지킷(Engage Kit) 개발기: 시청자 참여를 이끄는 백엔드 여정
소스(Sauce)의 개발본부 팀(프론트, 백엔드)은 최근 라이브커머스 시청자들의 적극적인 참여를 유도하는 핵심 프로덕트인 '인게이지킷(Engage Kit)'을 성공적으로 개발하여 런칭했습니다.
이 글에서는 인게이지킷이 무엇인지부터 백엔드 개발 과정에서 겪었던 기술적 난관과 해결책, 그리고 최적의 성능을 위한 데이터베이스 선정과 공정한 실시간 추첨 시스템 구축하기까지 깊이 있게 다뤄보려고 합니다.
1. 인게이지킷(Engage Kit)이란?
라이브커머스에서 '인게이지킷'은 시청자들의 적극적인 참여와 상호작용을 유도하기 위해 사용되는 다채로운 기능 및 도구 모음을 의미합니다. 단순한 상품 판매를 넘어, 시청자들이 방송에 직접 참여하며 즐거운 경험을 할 수 있도록 돕는 것이 핵심 목표입니다.
소스팀이 개발한 인게이지킷은 크게 두 가지 핵심 이벤트인 구매 인증과 퀴즈 기능을 포함하고 있습니다. 이러한 인게이지킷은 시청자의 몰입도를 높여 구매 전환율을 극대화하고, 방송에 대한 재미와 흥미를 유발하여 장기적인 고객 충성도를 구축하는 데 기여합니다.
2. 라이브커머스 실시간 퀴즈 이벤트
소스라이브 인게이지킷의 핵심 기능 중 하나인 퀴즈 이벤트는 시청자들에게 실시간으로 질문을 던지고, 정답을 맞힌 참여자에게 혜택을 제공하여 방송의 재미를 더합니다. 이 퀴즈 이벤트를 플레이어에 정확한 타이밍에 띄우는 것이 중요한 기술적 과제였습니다.
2.1. 초반 시도: AWS MediaLive ID3 Tag의 한계
초기에는 AWS MediaLive가 제공하는 ID3 Tag를 활용하여 플레이어에 퀴즈 이벤트 시작을 알리려고 했습니다. ID3 Tag는 오디오/비디오 스트림 내에 메타데이터를 삽입하는 표준 방식으로, 실시간 스트림에 퀴즈 시작 신호를 넣어 플레이어가 이를 감지하고 퀴즈 팝업을 띄우는 방식이었습니다.
하지만, 이 방식은 iOS 환경에서 제대로 동작하지 않는 문제에 직면했습니다. iOS 기기에서는 HLS(HTTP Live Streaming) 프로토콜을 사용하여 비디오를 스트리밍하는데, HLS 재생 시 ID3 Tag가 항상 안정적으로 파싱되지 않거나, 지연 또는 누락되는 경우가 발생했습니다. 이로 인해 퀴즈 시작 타이밍이 어긋나는 치명적인 문제가 있었습니다.
2.2. 소켓 기반의 퀴즈 동기화: 정확성과 안정성 확보
iOS 이슈와 안정적인 퀴즈 동기화를 위해 소켓(WebSocket) 통신을 활용한 새로운 방식을 고안했습니다. 퀴즈 이벤트가 시작되면, 백엔드에서는 시청하는 모든 유저의 플레이어에게 소켓을 통해 이벤트 정보 전달을 하도록 하였습니다.
이 방식으로 플레이어는 백엔드로부터 받은 정보를 바탕으로 현재 재생 중인 영상에서 퀴즈가 시작되어야 할 정확한 시점에 퀴즈 팝업 모달을 띄울 수 있게 되었습니다. 이는 네트워크 지연이나 플레이어 버퍼링과 상관없이 모든 시청자에게 거의 동시에 퀴즈가 노출될 수 있도록 하여 공정한 참여 기회를 제공했습니다.
3. 트래픽을 고려한 데이터베이스 선정: DynamoDB와 MySQL
인게이지킷 퀴즈 이벤트는 단일 방송에 수만에서 수십만 명의 시청자가 동시다발적으로 참여하여 퀴즈를 풀고 답변을 제출하는 등 엄청난 양의 요청을 처리해야 합니다. 이러한 대규모 동시 접속 트래픽을 안정적으로 감당할 수 있는 데이터베이스 선정이 매우 중요했습니다.
3.1. DynamoDB 선택 이유
소스팀은 영속적으로 저장해야 할 참여자 데이터 관리를 위해 DynamoDB를 선택했습니다. DynamoDB는 AWS에서 제공하는 NoSQL 키-값(Key-Value) 데이터베이스로, 뛰어난 성능과 확장성 덕분에 대규모 트래픽 처리에 적합하다고 판단했습니다.
뛰어난 성능과 확장성: 갑작스럽게 늘어나는 라이브커머스 트래픽에 유연하게 대응할 수 있습니다.
단일 액세스 패턴(Single Access Key Pattern) 적용 용이: 데이터 액세스 패턴에 효율적으로 대응할 수 있었습니다. 예를 들어, Partition Key를 event_id, Sort Key를 member_id로 설정하여 특정 이벤트의 참여자 데이터를 빠르게 조회하도록 설계했습니다.
3.2. DynamoDB 사용 시 단점: 트랜잭션 처리의 복잡성
DynamoDB는 강력한 성능을 제공하지만, RDB와 JPA가 제공하는 편리함에 익숙한 개발자에게는 트랜잭션 관리의 복잡성이 가장 큰 단점으로 다가옵니다.
기존 MySQL(RDB) 환경에서는 Spring Data JPA의 @Transactional 어노테이션 하나로 메소드 전체를 원자적인 트랜잭션 단위로 손쉽게 묶을 수 있었습니다. 하지만 DynamoDB SDK를 사용하면서, 아래 예시 코드와 같이 트랜잭션을 위해 고려할 점이 많아지고 코드의 복잡도가 크게 증가했습니다.
이 코드가 보여주는 복잡성은 단순히 문법의 차이가 아니라, 데이터베이스를 다루는 패러다임의 차이에서 비롯됩니다. JPA가 '선언적' 방식이라면, DynamoDB SDK는 '명령형' 방식에 가깝습니다. 코드의 구체적인 지점을 통해 이 복잡성을 살펴보면 다음과 같습니다.
1. 수동 배치 처리: DynamoDB의 TransactWriteItems API는 한 번에 최대 100개(이전 25개)의 아이템만 처리할 수 있다는 제약이 있습니다. 코드의 BATCH_SIZE와 for문처럼, 개발자가 이 제약사항을 직접 인지하고 데이터를 분할하여 처리하는 로직을 구현해야 합니다.
2. 트랜잭션 작업의 명시적 구성: transactItems.add(...) 부분처럼, 당첨자 정보를 생성(Put)하고, 참여자 정보의 두 키(Key)를 모두 업데이트(Update)하는 세 가지의 다른 작업을 각각 TransactWriteItem 객체로 명시적으로 만들어 리스트를 구성해야 합니다. JPA였다면 각 객체의 상태를 변경하는 것만으로 프레임워크가 '변경 감지(Dirty Checking)'를 통해 알아서 처리해 주었을 것입니다.
3. 구체적인 예외 처리: TransactionCanceledException을 별도로 catch 하는 것처럼, DynamoDB의 트랜잭션이 왜 실패할 수 있는지 그 원인을 더 깊이 이해하고 직접 예외 처리를 구현해야 하는 학습 곡선이 존재합니다.
물론 이러한 복잡성은 개발 생산성을 저하시키는 명백한 단점입니다. 하지만 소스팀은 개발 편의성을 일부 희생하는 대신, 수십만 동시 접속자가 몰리는 라이브 퀴즈 이벤트의 요청을 지연 없이 처리할 수 있는 DynamoDB의 압도적인 성능과 확장성을 얻고자 했습니다. 이는 서비스의 핵심 요구사항을 충족하기 위한 의도적인 기술적 트레이드오프였습니다.
4. 휘발성 데이터 처리: Redis를 활용한 임시 당첨자 저장
퀴즈 이벤트는 수많은 참여자 중 최종 당첨자를 추첨하는 과정을 거칩니다. 이 과정에서 당첨자 발표 전까지의 '임시 당첨자' 데이터는 휘발성 데이터로 간주했습니다. 즉, 최종 당첨자가 확정되기 전까지는 빠르게 접근하고 빠르게 사라져도 무방한 데이터라는 판단입니다.
소스팀은 이러한 임시 당첨자 데이터를 Redis 인메모리(in-memory) 데이터베이스에 저장했습니다.
왜 이렇게 했는가?
Redis에 임시 당첨자 데이터를 저장함으로써 얻는 가장 큰 이점은 메인 데이터베이스(DynamoDB)의 부하를 줄이고, 최종 당첨자 추첨 로직을 효율적으로 분리할 수 있다는 점입니다. Redis의 초고속 읽기/쓰기 성능과 TTL(Time To Live) 기능은 실시간 추첨 로직을 빠르고 효율적으로 처리하는 데 최적의 환경을 제공했습니다.
5. 공정한 당첨자 추첨: 저수지 샘플링(Reservoir Sampling) 알고리즘의 적용
퀴즈 이벤트가 성공적으로 끝나면 DynamoDB에는 적게는 수만, 많게는 수십만 명에 달하는 정답자 데이터가 저장됩니다. 이제 이 거대한 데이터 목록에서 정해진 수(K)의 당첨자를 공정하게 추첨해야 하는 과제가 남았습니다.
5.1. 왜 일반적인 랜덤 추첨은 위험한가?
가장 단순한 방법은 DynamoDB에서 모든 정답자 목록을 조회하여 애플리케이션 서버의 메모리에 올린 뒤, Collections.shuffle() 같은 라이브러리를 사용해 무작위로 K명을 뽑는 것입니다. 하지만, 이 방식은 참여자 수가 많아질 경우 OutOfMemoryError를 유발하며 서버를 다운시킬 수 있는 매우 위험한 방법입니다.
소스팀은 서버의 메모리 사용량을 예측할 수 있고 안정적으로 유지하면서, 대용량의 참여자 목록에서 공정한 샘플링을 해야 했습니다. 이 문제를 해결하기 위해 저수지 샘플링(Reservoir Sampling) 알고리즘을 채택했습니다.
5.2. 저수지 샘플링: 모두에게 공정한 '스마트 추첨함'
수십만 명의 정답자 중에서 당첨자를 뽑기 위해 '저수지 샘플링'이라는 아주 스마트한 추첨 방식을 사용했습니다. 이 방식이 어떻게 동작하는지 '10명의 당첨자를 뽑는 추첨함'에 비유하여 설명해 드릴게요.
1단계: 추첨함 채우기
먼저, 가장 먼저 정답을 맞힌 10명의 참여권을 '당첨 후보 추첨함'에 넣어둡니다. 이제 추첨함은 10개의 참여권으로 가득 찼습니다.
2단계: 새로운 참여권의 등장과 '자격 교체'
11번째 정답자가 등장했습니다. 이 사람도 당첨될 기회가 있어야 공정하겠죠?
그래서 컴퓨터는 '이 11번째 사람에게도 기회를 줄까?' 하고 아주 공정한 확률 계산을 합니다.
계산 결과, '기회를 주자!'로 결정되면, 이미 추첨함에 있던 10개의 참여권 중 하나를 무작위로 빼내고, 그 자리에 11번째 사람의 참여권을 집어넣습니다. '기회를 주지 말자'로 결정되면 11번째 참여권은 그냥 지나칩니다.
3단계: 모든 참여권에 대한 공정한 기회 부여
이 과정은 12번째, 13번째, ... 그리고 마지막 10만 번째 정답자가 나타날 때까지 계속 반복됩니다.
중요한 점은, 뒤늦게 참여한 사람일수록 추첨함에 들어갈 확률은 조금씩 낮아지지만, 마지막에 참여한 사람에게도 교체될 기회는 반드시 주어진다는 것입니다.
마찬가지로, 초반에 추첨함에 들어갔던 사람도 계속해서 새로운 참여권과 교체될 수 있기 때문에 마지막까지 안심할 수 없습니다.
최종 결과: 공정하게 뽑힌 당첨자들
모든 정답자들의 참여권을 이런 방식으로 한 명씩 모두 확인하고 나면, 최종적으로 '당첨 후보 추첨함'에 남아있는 10명이 진짜 당첨자가 됩니다.
이 방식은 참여 순서와 상관없이 모든 사람에게 공정한 기회를 제공하면서, 컴퓨터의 메모리를 거의 사용하지 않는 아주 똑똑한 방법입니다.
5.3. 저수지 샘플링 결론
압도적인 메모리 효율성: 이 방식의 가장 큰 장점입니다. 참여자가 100만 명이든 1,000만 명이든, 서버는 오직 K개의 당첨 후보 데이터만 메모리에 유지하면 됩니다. 이를 통해 예측 불가능한 대규모 데이터에도 안정적으로 추첨 기능을 수행할 수 있습니다.
공정성 보장: 저수지 샘플링은 수학적으로 모든 참여자가 최종 당첨자로 뽑힐 확률(K/N, N은 전체 참여자 수)을 동일하게 보장하는 알고리즘입니다.
구현의 용이성: DynamoDB에서 Scan이나 Query의 결과를 페이지 단위로 받아오면서, 각 아이템에 대해 저수지 샘플링 로직을 순차적으로 적용하면 되므로 구현이 복잡하지 않습니다. 전체 데이터를 한 번만 순회(Single Pass)하면 되므로 효율적입니다.
이처럼 이벤트 참여 데이터를 DynamoDB에 안정적으로 저장하고, 추첨 시에는 저수지 샘플링 알고리즘을 통해 메모리 문제를 원천적으로 해결함으로써, 수십만 명이 참여하는 대규모 이벤트에서도 빠르고 공정한 당첨자 추첨 시스템을 구축할 수 있었습니다.
마치며
이번 인게이지킷 개발은 라이브커머스 환경에서 대규모 동시 접속과 실시간 데이터 처리라는 쉽지 않은 기술적 도전을 요구했습니다. ID3 Tag의 한계를 넘어 소켓 기반의 동기화 시스템을 구축하고, DynamoDB와 Redis를 전략적으로 활용했으며, 특히 저수지 샘플링 알고리즘을 통해 공정하고 효율적인 추첨 시스템을 완성한 과정은 소스 백엔드 팀에게 값진 경험이었습니다.
시청자들의 뜨거운 참여와 반응을 보며, 개발 과정의 어려움이 눈 녹듯 사라지는 것을 느낍니다. 앞으로도 소스팀은 안정적이고 확장 가능한 백엔드 시스템을 통해 더욱 즐겁고 몰입감 넘치는 라이브커머스 경험을 제공하기 위해 끊임없이 노력하겠습니다.