테크

2025. 04. 29

Redis를 활용한 배너 광고 이벤트 수집 구조 개선기

서비스 안정성과 성능 향상을 향한 여정

Redis를 활용한 배너 광고 이벤트 수집 구조 개선기Redis를 활용한 배너 광고 이벤트 수집 구조 개선기

지난 1월, 에브리타임에 ‘혜택탭’을 새롭게 런칭하며 사용자가 앱 내에서 바로 에브리유니즈로 이동할 수 있도록 했고, 그 결과 전체 트래픽이 평균 대비 약 30% 증가하는 효과를 얻을 수 있었습니다.


트래픽이 급격하게 늘어나면서 자연스럽게 서버와 데이터베이스에도 부하가 발생했는데요. 특히 이벤트나 신제품 출시 시점에는 체감될 정도로 서비스가 느려지는 현상이 나타났습니다.


이를 개선하기 위해, 서비스에서 가장 자주 호출되는 API 중 하나인 ‘배너 광고 성과 수집 API’의 구조를 점검하고 최적화 작업을 진행했습니다. 그 결과 인스턴스 성능을 약 20% 개선할 수 있었는데요. 이제부터 어떤 문제를 마주했는지, 그리고 어떻게 해결해 나갔는지를 자세히 풀어보겠습니다.



1️⃣ 배너 광고 성과 수집 API의 구조와 문제점

하루 평균 약 70만 건이 호출되는 배너 광고 성과 수집 API는, 사용자가 배너를 노출하거나 클릭할 때마다 해당 배너의 식별값과 날짜를 기준으로 값을 +1 하는 단순한 구조로 작동합니다.



하지만 트래픽이 늘어나면서, 단순해 보였던 이 로직에서도 여러 문제가 발생하기 시작했습니다.


문제 1) 쏟아지는 트래픽이 버거운 데이터베이스

트래픽이 급격히 증가하면서, 광고 성과 수집 API의 호출 횟수도 함께 폭발적으로 늘어났습니다. 초당 수~수십건에 이르는 지속적인 SQL UPDATE 요청이 몰리면서, 데이터베이스에 과도한 쓰기 부하가 발생했습니다.


그 결과 전체 서비스의 응답 속도가 점점 느려졌고, 시간이 지날수록 성능 저하와 병목 현상이 심화되는 상황으로 이어졌습니다.


문제 2) 동시 업데이트로 인한 충돌

두 번째 문제는 업데이트 락(Update Lock) 경합입니다. 에브리유니즈는 MySQL을 사용하고 있으며, 배너 성과 데이터는 (배너 ID, 날짜)를 기본키(PK)로 하여 집계하고 있습니다. 이 구조에서는 같은 배너에 대한 요청이 동시에 들어오면, 같은 행(Row)을 동시에 업데이트하려는 충돌이 발생합니다.


그로 인해 Row-level Lock 충돌이 자주 일어났고, 처리 대기열이 길어지면서 TPS(초당 처리 건수)도 제한을 받게 되었습니다. 특히, 많은 트랜잭션이 동시에 발생할 경우 Deadlock(교착 상태)이 생기고, 업데이트가 롤백되거나 재시도가 필요해지는 문제도 함께 발생했습니다.


이러한 문제를 해결하기 위해 커머스개발팀 담당자들은 최적의 해결 방안을 찾아야만 했습니다.



2️⃣ 광고 성과 수집 안정화를 위한 최적의 해결방안을 찾아라

해결방안 1) 배너 광고 성과 데이터 구조 변경

기존에는 배너 광고 데이터를 날짜별로 누적하는 방식으로 관리했습니다. 예를 들어, 특정 배너가 어떤 날에 노출되거나 클릭되면 해당 날짜의 기록을 찾아 impression이나 click 값을 업데이트하는 식이었습니다.


기존 테이블 구조는 다음과 같습니다.

CREATE TABLE banner_performance (
  banner_id      INT NOT NULL,
  date      DATE NOT NULL,
  impression    BIGINT DEFAULT 0,
  click         BIGINT DEFAULT 0,
  PRIMARY KEY (banner_id, date)
) ENGINE=InnoDB;


이 방식은 동일한 행(row)에 계속 값을 누적하는 구조이기 때문에, 앞서 언급했던 문제들이 발생하는 단점이 있었습니다. 이를 해결하기 위해 구조를 다음과 같이 변경하면 Lock 문제를 피할 수 있고, 저장된 데이터를 배치 작업으로 나중에 집계하면 되기 때문에 처리 효율도 높아집니다. 이벤트가 발생할 때마다 새로운 행을 추가하는 방식으로 전환하기 때문입니다.


CREATE TABLE banner_performance (
  id             INT NOT NULL PRIMARY KEY
  banner_id      INT NOT NULL,
  event_type     VARCHAR(10) NOT NULL
  date      DATE NOT NULL
) ENGINE=InnoDB;


즉, 배너가 노출되거나 클릭될 때마다 그 이벤트를 개별적으로 저장하며, 같은 배너나 날짜라도 중복 없이 계속 데이터를 누적해 나갈 수 있습니다. 또한 이벤트 단위로 데이터가 저장되어서 더 다양한 형태의 분석이 가능해지는 장점이 있습니다.


해결방안 2) 메시지 큐 기반 비동기 처리

두 번째로 떠올린 방안은 메시지 큐를 활용한 비동기 처리 방식입니다. API 요청이 들어오면 데이터를 바로 데이터베이스에 저장하지 않고, 먼저 메시지 큐(SQS)에 적재합니다. 이후 EventBridge를 통해 정해진 주기에 따라 Lambda가 실행되고, 큐에 쌓인 메시지를 읽어 데이터베이스(RDS)에 반영하는 구조입니다.


아래는 이 구조의 대표적인 형태입니다.



이 방식을 사용하면 API 요청에 대한 응답을 빠르게 반환해 사용자 체감 속도가 개선됩니다. 또한, 데이터베이스에 직접적으로 실시간 접근을 하지 않기 때문에, SQL 호출이 분산되고 DB 부하를 줄일 수 있는 장점이 있습니다.


하지만 단점도 있습니다. 메시지가 처리 중에 실패하거나 유실되는 경우를 대비한 재시도 로직이 필요하다는 점입니다. 큐의 상태나 Lambda 실패 등을 모니터링하고 대응해야 하기 때문에, 운영 복잡도가 증가할 수도 있습니다.


해결방안 3) Redis를 활용한 일간 데이터 집계

세 번째 방법은 Redis를 중간 저장소로 활용해 데이터를 실시간으로 누적하고, 이후 일정 주기로 한꺼번에 데이터베이스에 저장하는 방식입니다.



API 서버는 광고 노출이나 클릭 같은 이벤트가 발생할 때마다 이를 실시간으로 Redis에 저장합니다. Redis는 메모리 기반 저장소이기 때문에 처리 속도가 빠르고, 클라이언트 요청에 대해서는 싱글스레드로 동작하기 때문에 Lock 경합 없이 데이터를 계속 누적할 수 있습니다.


이후 하루에 한 번, Lambda가 실행되어 Redis에 누적된 데이터를 불러오고, 이를 RDS로 이전합니다. 이 과정에서 하루 동안 쌓인 이벤트 수를 집계하여 날짜별, 배너별 성과 데이터를 저장하게 됩니다.


이렇게 세 가지 방안을 바탕으로 SQL 호출 횟수, Row Lock 가능성, 그리고 개발 및 운영 비용 측면을 중심으로 비교표를 작성해, 보다 객관적으로 가장 적합한 해결책을 도출하고자 했습니다.



👉 해결 방안 1

구조적으로 성능 문제를 해소할 수 있다는 장점이 있지만, 데이터 적재량 증가로 인한 비용 부담과 어드민 리포트 등 관련 시스템 전반의 수정이 필요해 개발 리소스와 인프라 비용 측면에서 부담이 큼

👉 해결 방안 2

API 응답 속도 개선과 DB 부하 감소로 속도는 빨라지지만, 메시지 유실이나 재처리 등으로 인해 운영이 복잡함

👉 해결 방안 3

기존 인프라를 그대로 활용하면서도, 성능·비용·안정성 모두 균형 잡힘


장단점을 비교한 결과, ‘Redis를 활용한 일간 데이터 집계 구조’가 현재 서비스 환경에 가장 적합한 방법이라는 결론에 도달할 수 있었습니다. 그렇다면, Redis는 실제로 어떻게 활용되었을까요?



3️⃣ Redis 적용 구조 살펴보기


새로운 구조에서 각 API 서버는 광고 노출 또는 클릭 이벤트가 발생할 때마다 해당 데이터를 Redis에 집계합니다. 그리고 트래픽이 가장 적은 시간대인 오전 6시에 AWS EventBridge Scheduler가 Lambda를 호출해 Redis에 누적된 전일 데이터를 읽어온 뒤, 이를 가공해 데이터베이스에 저장하는 방식입니다.



이때 Redis에는 날짜와 배너 ID를 조합한 하나의 Key를 만들고, 그 안에 IMPRESSION, CLICK 같은 필드를 각각 +1씩 증가시키는 구조를 사용했습니다. Redis의 HINCRBY 명령어를 활용하면 동시 요청이 많아도 Lock 경합 없이 안정적으로 값을 누적할 수 있기 때문에, 실시간 집계에 매우 적합한 방식이었습니다.


처음에는 각 항목을 개별 String Key로 저장하는 방안도 고려했지만, Redis의 Key 수가 많아질수록 메모리 사용량이 늘어나고 관리가 복잡해질 수 있다는 점을 고려해 하나의 Hash Key로 통합하는 방식으로 설계했습니다.



한편, 하루에 한 번 실행되는 Lambda 함수는 Redis에서 데이터를 읽어와 RDS(MySQL)에 벌크 삽입한 뒤, 일주일 이상 된 데이터를 정리합니다. 이때 Redis의 KEYS 명령어 대신 SCAN을 사용해 데이터를 탐색하는데요. SCAN은 반복적으로 점진 탐색을 수행하기 때문에, Redis의 싱글 스레드 처리 특성상 부하를 최소화할 수 있습니다. 실제 운영 환경에서는 수십만 개의 Key가 존재할 수 있기 때문에, 안정성과 효율성을 모두 고려한 선택이었습니다.





4️⃣ 적용 후 개선 효과에 대하여

Redis를 활용하면서 가장 눈에 띄게 개선된 부분은 RDS의 성능입니다. CPU 사용률은 약 20% 감소했고, Write IOPS*는 무려 10배 이상 줄어들었습니다.

*IOPS(Input/Output Operations Per Second)는 저장장치(HDD, SSD 등)의 성능을 나타내는 단위로, 초당 입출력 작업 수를 의미



이는 반복적인 SQL 업데이트 작업이 Redis로 대체되면서, RDS가 감당해야 할 쓰기 작업이 크게 줄어든 결과입니다.


DB 부하가 줄어들면서, API 서버 성능도 함께 개선됐습니다. EC2의 CPU 사용률은 약 8% 감소했고, 응답 속도 또한 눈에 띄게 개선됐습니다.



새롭게 배너 데이터까지 Redis에 저장하게 되었지만, ElastiCache 인스턴스의 CPU나 메모리 사용률에는 유의미한 영향이 없었습니다. 이는 Redis의 경량화된 처리 방식과 메모리 기반 구조 덕분에 가능했던 부분입니다.



이번 Redis 도입을 통해 단순한 구조 개선만으로도 데이터베이스의 부하를 크게 줄이고, 응답 속도와 TPS를 향상시키는 효과를 얻을 수 있었습니다. 동시에 성능 저하와 DB Lock 경합 같은 근본적인 문제도 자연스럽게 해소되었습니다.


무엇보다도 이번 경험을 통해, 단순 카운터 누적에는 RDBMS보다 인메모리 데이터 스토어가 훨씬 더 적합하다는 사실을 다시금 확인할 수 있었습니다. 이와 함께, 현재의 문제에 가장 적합한 도구를 유연하게 선택하는 것이 서비스 안정성과 성능 향상을 위한 가장 핵심 전략임을 깨닫기도 했습니다.


앞으로도 지속적인 구조 개선과 기술 최적화를 통해 에브리유니즈를 더 안정적이고 빠른 서비스로 발전시켜 나가겠습니다.


-


Written by 방근호 | 비누커머스 백엔드 개발자

복잡한 문제도 쉽게 풀어내기 위해 오늘도 고민하고 노력합니다.