Rolling MAU, 불가능을 가능의 영역으로 만드는 방법

Joshua Kim
IOTRUST : Team Blog
10 min readJun 29, 2024

들어가는 글

Rolling Metrics는 계산 과정이 복잡할 뿐더러, 쿼리 요청 시간과 비용이 막대하게 들기 마련입니다. 데이터 분석 목적을 지닌 사람이 아니라면 이 과정이 얼마나 복잡한지 알기 어려운 영역이기도 한데요. 특히 트래픽이 굉장히 많은 프로덕트 데이터를 다루는 기업이라면 이 지표를 추출하거나 관리하는 것을 비용 측면에서 불가능한 것으로 치부하기도 합니다. 실제로 몇몇 유니콘 기업의 데이터팀들에서는 Rolling Metrics를 지표 딜리버리 정책에서 제외하고 있기도 합니다.

이번 아티클에서는 Rolling Metrics의 사례인 Rolling MAU를 불가능한 영역으로부터 가능의 영역으로 만든 문제 해결 방법을 소개해드리고자 합니다.

개념

Rolling Metrics란 계산의 기준이 말 그대로 “굴러가는” 특성을 지닌 지표를 의미합니다. “굴러가는” 기준을 흔히 Window라고 표현하기도 하는데요. 대표적으로 다음 두 가지 지표가 Rolling Metrics에 해당합니다.

(1) Rolling Stickiness

(2) Rolling MAU

  • 특정 월(MM)의 활성 사용자 수를 Static하게 계산하는 것이 아니라, 각 일자별로 Recent 30-day Window 내의 활성 사용자 수를 계산하는 방법을 의미합니다.

케이스 스터디

가령 A라는 프로덕트에 활성화된 사용자 목록이 다음과 같다고 가정한 후, Rolling 2-day Active Users 지표를 계산해보도록 하겠습니다.

필자 작성

즉, Window에 해당하는 화살표를 일자별로 이동하며 “순수 사용자 수”를 계산하게 되는 것입니다.

문제의 시작

그렇다면, Rolling MAU 지표를 계산하는 과정에서 왜 막대한 쿼리 비용과 시간이 소모되는 것일까요? 이를 이해하기 위해 우선 제가 처음에 작성했던 샘플 쿼리문을 뜯어보도록 하겠습니다.

SELECT
MAIN.date,
COUNT(DISTINCT SUB.user_id) AS rolling_mau
FROM
daily_activated_users MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date
GROUP BY
MAIN.date
ORDER BY
MAIN.date

다양한 작성 방법이 존재하겠지만, Rolling MAU 지표를 계산하기 위해서는 위와 같이 Self Join, Subquery 등을 통해 불가피하게 기하급수적 연산을 할 수밖에 없습니다. 즉, 아래 그림과 같이 O(n²)에 해당하는 연산에 해당하므로 프로덕트 내 사용자 수와 연산 비용은 거듭 제곱의 관계에 놓일 수밖에 없는 것이죠.

Big O Notation

왜 거듭제곱의 관계에 놓이는 건가요?

가령, A라는 프로덕트 케이스를 대상으로 위 쿼리문의 내부 연산 과정을 뜯어보도록 하겠습니다.

(1) 먼저, 아래 과정을 통해 daily_activated_users 테이블의 데이터를 가져옵니다.

FROM
daily_activated_users MAIN
필자 작성

(2) 그런 후, SELF JOIN을 통해 각 일별 Recent 2-day 활성 사용자 목록을 모두 이어 붙입니다.

FROM
daily_activated_users MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '1 DAYS' AND MAIN.date
필자 작성

(3) 이제 MAIN.date 을 기준으로 그룹화하여 순수 사용자 수를 계산합니다.

SELECT
MAIN.date,
COUNT(DISTINCT SUB.user_id) AS rolling_mau
FROM
daily_activated_users MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date
GROUP BY
MAIN.date
필자 작성

이 과정의 정확한 문제점

연산 시간이 가장 많이 소모되는 지점은 바로 과정 (2)입니다. 이 과정은 하나의 행에 Recent 2-day Window에 해당하는 모든 행들을 이어붙이는 과정인데요. 예를 들어, 1월 2일의 행 수가 10개이고, Recent 2-day Window에 해당하는 행이 모두 100개 라면, 총 1,000개의 행( 10*100 )을 이어 붙여야 합니다.

즉, SELF JOIN을 통해 각 일별 Recent 2-day 활성 사용자 목록을 모두 이어 붙이는 과정이 Scan 시간과 메모리 사용량을 상당히 많이 잡아먹고 있는 것어었죠.

실제로, 전체 테이블에서 Rolling MAU를 계산하는 쿼리 시간이 6시간 정도 소모되었고, 데이터 마트를 구축하여 Incremental Strategy를 통해 증분적으로 계산하는 과정 조차도 2시간 가까이 소모되었습니다. 제품에 녹여내기 위한 고민이 깊어진 것입니다.

문제 해결 전략

요약하면, 총 두 가지 방법을 통해 위 문제를 해결할 수 있었습니다.

  • date 칼럼에 B-tree Index를 생성하여 Scan 속도 향상
  • 이어 붙이는 과정에서 메모리 사용량을 줄이기 위해 MAIN 테이블에서 필요한 칼럼만 불러오기

(1) date 칼럼에 B-tree Index를 생성하여 Scan 속도 향상

아래 과정을 진행하면서 date 칼럼을 비교하는 과정이 많은 시간이 소요되는 지점임을 확인했고, 결국 date 칼럼을 Index로 두어 스캔 속도가 향상될 수 있도록 했습니다.

FROM
daily_activated_users MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date

(2) 이어붙이는 과정에서 메모리 사용량을 줄이기 위해 MAIN 테이블에서 필요한 칼럼만 불러오기

안타깝게도 date 칼럼을 Index로 생성했음에도 불구하고 쿼리 시간 문제가 거의 해결되지 않았습니다. 즉, date 칼럼을 비교하는 과정이 아니라, 결국 O(n²)에 해당하는 거듭제곱 만큼 메모리를 사용해야 하는 과정이 핵심 문제였기 때문입니다. 따라서, 메모리를 줄이기 위해 MAIN 테이블에서 반드시 필요한 칼럼만을 불러오는 방법을 고안했습니다.

SELECT
MAIN.date,
COUNT(DISTINCT SUB.user_id) AS rolling_mau
FROM
(SELECT DISTINCT date FROM daily_activated_users) MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date
GROUP BY
MAIN.date
ORDER BY
MAIN.date

즉, MAIN 테이블에서 모든 행을 다 불러오는 것이 아니라, 순수 date 칼럼 만을 불러옴으로써 거듭제곱의 부담을 극적으로 경감시켰던 것입니다. SELF JOIN을 통해 각 일별 Recent 2-day 활성 사용자 목록을 모두 이어 붙이는 과정이 아래와 같이 극적으로 줄어들었습니다.

필자 작성

문제 해결 전과 후 비교

문제 해결 전 전체 테이블 대상으로 실제 쿼리 시간은 6시간 정도였으나, 문제 해결 후 6초로 줄어들었습니다. 즉 정리하면 다음의 이유로 인해 드라마틱하게 줄어들었던 것입니다.

  • date 칼럼을 B-tree Index로 생성함으로써 아래의 date 비교 연산 부담이 소폭 줄어들었다.
    ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date
  • MAIN 테이블에서 최소한으로 필요한 칼럼만을 불러옴으로써 SELF JOIN의 거듭 제곱 메모리 사용량이 극적으로 줄어들었다.
FROM
(SELECT DISTINCT date FROM daily_activated_users) MAIN
LEFT JOIN
daily_activated_users SUB
ON SUB.date BETWEEN MAIN.date - INTERVAL '29 DAYS' AND MAIN.date

나가는 글

사실 Rolling Metrics는 많은 데이터팀에서 끌어안기보다는 회피하려는 성격을 지니고 있습니다. 이 지표가 가져올 가치에 비해 인프라 비용이 지나치게 많이 들기 때문이죠. 실제로 Amplitude에서도 DAU, WAU, MAU를 계산하기 위한 기본 옵션을 Rolling 방식이 아니라 캘린더 기준의 Static 방식을 제공하고 있습니다. 내부 쿼리 실행 시간과 속도의 문제를 회피하기 위한 의도가 숨어있겠죠.

Amplitude의 Weekly, Monthly, Quarterly 옵션은 모두 캘린더 기준의 Static 방식입니다.

물론, 아래와 같이 사용자 정의 함수를 통해 의도적으로 Rolling Metrics를 적용할 수 있다는 건 비밀입니다.🙃

Amplitude에서 ROLLWIN 함수를 사용하면 Window를 지정하여 계산할 수 있습니다.

아무튼, Rolling MAU 지표는 쿼리 최적화를 신경 써야 하는 중요한 영역 중 하나였으며, 이에 대한 레퍼런스가 전혀 없는 상황에서 스스로 부딪히며 고민하며 “가능의 영역”으로 이끌어낼 수 있었습니다. Rolling Metrics는 데이터의 Freshness를 향상시키고 데이터를 활용하는 사용자의 기민한 대응에 기여할 수 있는 만큼 중요한 문제 해결을 이룰 수 있었습니다.

--

--