시작하며
🔗 if(kakao) 발표 영상 보러 가기: Batch Performance 극한으로 끌어올리기
지난 게시글에서는 배치(batch processing, 일괄 처리)로 대량의 데이터를 읽을 때 어떻게 개발해야 할지 알아보았습니다. 이번 게시글에서는 대량 데이터를 Aggregation 하는 방법에 대해 알아보고자 합니다. 개발자들은 Aggregation 즉, 집계를 해야 하는 상황에 배치로 개발을 많이 합니다. 그리고 이런 Aggregation 배치를 만들 때 대부분 GroupBy, Sum, Count 구문을 사용한 쿼리를 만들어 집계 연산을 DB에 위임하여 처리합니다. 이런 방식이 가지고 있는 문제를 살펴보고 어떻게 문제를 극복했는지 공유하고자 합니다.
배치로 개발하려는 상황 중 하나, 통계 생성
개발자들은 데이터를 집계할 때 주로 배치로 개발합니다. 집계란 N개의 데이터를 1개의 데이터로 요약하는 것이며 보통은 통계 형식의 데이터를 만들 때 사용합니다. 숫자를 누적하고 세는 게 주요 목적입니다.
통계 데이터는 보통 실시간(Real-Time)으로 생성하기에는 부적합한 것으로 인지되고 있습니다. 실시간으로 통계 데이터를 생성한다면 여러 문제가 발생합니다.
- 건 데이터 저장과 통계 데이터 수정을 동시에 처리하도록 구현하면 성능상 병목이 발생함 (통계 데이터의 동시성 처리)
- 건 데이터를 수정하거나 삭제하였을 때 통계 데이터를 수정하기 까다로움
일단위 통계라면 하루치 데이터가 확정된 이후인 다음 날 생성하는 것이 쉽고 일반적입니다. 예를 들자면, 일간 사용자의 연령별 구매 횟수와 금액을 구한다고 하면 매일 1회 배치를 통해 데이터를 누적하는 것이 쉬운 개발 방식입니다.
서버 개발자들은 어떻게 통계 배치를 개발할까?
서버 개발자들이 일반적으로 배치를 개발하는 방법을 먼저 소개하려 합니다. 사용자 나이별 주문 내역 통계 데이터를 생성하는 예시를 통해 일반적인 개발 방식을 살펴보겠습니다.
요구사항
마케팅 목적으로 사용자 나이별 주문 내역 통계 데이터 저장하기
- 사용자별, 상품별 금액을 SUM하여 마케팅 지표로 사용하기 위해 저장함
- 사용자별, 상품별 제품 구입 현황 조사
- 세부 요구사항
- 기준 데이터: 사용자 & 상품 ID
- 추출 데이터: 상품 금액 합산, 주문 횟수
관련 데이터
- Order: 주문 정보
- Price: 금액 정보
- User: 사용자 정보
일반적인 구현 방식
보통 이런 통계 데이터를 생성하는 배치에서는 어쩔 수 없이 GroupBy를 사용한 SUM 쿼리를 사용합니다. 애플리케이션과 MySQL 두 개만 사용하는 경우에는 다른 방법은 없다고 봐도 무방합니다.
ItemReader
- SUM 기준: 제품ID, 사용자 연령을 기준으로 GroupBy
- 조회 대상: 주문가격 SUM, 주문 횟수 SUM, 제 품가격, 제품ID, 사용자 연령
- 읽는 순서: 제품등록일시, 사용자 연령 오름차순
- Page Size: 1000개
이때 일반적으로 아래와 같은 쿼리를 작성하여 데이터를 Read합니다.
select
sum(o.amount),
count(1),
p.price,
p.product_id,
u.age
from order o
inner join price p
on o.price_id = p.id
inner join user u
on o.user_id = u.id
where o.order_date = '2021-01-01'
and u.is_active = true
group by p.product_id, u.age
order by p.registered_at asc, u.age asc
limit 1000, 0
ItemWriter
ItemReader에서 읽어온 데이터를 저장합니다. (물론 ItemProcessor를 통해 Read한 데이터를 가공할 수도 있지만 이 부분은 개발자의 성향과 개발 상황에 따라 매우 다르므로 제외하겠습니다.)
- 데이터를 데이터베이스에 저장함
- 보통은 JpaItemWriter 또는 Custom ItemWriter에 JPA Repository를 사용해서 Write
orderStatisticsRepository.saveAll(items)
일반적인 통계 배치 개발의 문제점
위의 예시를 보면 배치의 ItemReader에서 sum, join, groupby, orderby와 같은 구문들을 사용하고 있습니다. 대다수 서버 개발자들은 데이터를 합산할 때 이런 방식으로 개발하기 때문에 매우 합리적인 개발 방식임에 틀림없습니다. 그러나, 데이터 개수가 수 천만 개 이상으로 매우 많은 상황에서도 문제가 없을까요? 위의 배치는 합산, Count 연산 자체를 쿼리로 구하는 매우 쿼리에 의존적인 모습을 보여주고 있습니다.
통계를 위한 쿼리를 만들다 보면 where, groupby, orderby 구문에서 join한 각기 다른 테이블의 컬럼을 사용하는 경우가 있습니다. 이런 경우에는 실행계획을 예측하기 어렵습니다. 인덱스를 건다고 해도 실행 계획을 보면 대부분 Temporary Table을 생성하거나 Filesort를 사용하게 됩니다. 이는 극심한 조회 성능 저하로 이어집니다.