본문 바로가기
백엔드 개발

JPA MyBatis 집계 한계와 Spring Batch로 해결하는 대용량 집계 처리

by collenkim 2026. 4. 9.
반응형

서비스를 운영하다 보면 결국 통계 요구가 붙는다.
일별 매출, 주간 사용자 수, 월별 가입자처럼 집계 데이터는 처음에는 간단한 쿼리로 처리할 수 있지만, 데이터가 쌓이기 시작하는 순간 가장 먼저 한계에 부딪히는 영역이 된다.

보통은 JPA나 MyBatis로 집계 쿼리를 작성해서 처리한다. 하지만 일정 규모를 넘어가면 성능 저하, 메모리 문제, DB 부하 같은 이슈가 반복적으로 발생한다.
결론부터 말하면 이건 특정 기술의 문제가 아니라, 대용량 집계를 실시간으로 처리하려는 구조 자체의 한계다.


JPA와 MyBatis로 집계할 때 왜 문제가 생길까

JPA는 조회한 엔티티를 영속성 콘텍스트(1차 캐시)에 저장하는 구조다.
즉 데이터를 조회하는 순간 단순 값이 아니라 객체로 생성되어 메모리에 올라간다.

문제는 집계가 “많이 읽는 작업”이라는 점이다.
조회 → 엔티티 생성 → 메모리 적재가 반복되면서 데이터가 많아질수록 메모리 사용량이 증가하고, GC 부담이 커지며 심하면 OOM으로 이어진다.

여기에 집계를 서비스 로직에서 처리하면 트랜잭션도 비정상적으로 커진다. 수천~수만 건을 한 번에 처리하면서 하나의 트랜잭션으로 묶이기 때문에 DB 락 유지 시간이 길어지고, 롤백 비용도 증가한다. 장애가 발생하면 영향 범위 역시 커질 수밖에 없다.

또 하나는 JPQL의 표현 한계다. 단순한 SUM, COUNT는 문제없지만 실무에서는 조건별 집계, 다중 그룹핑, 중간 가공 같은 요구가 붙는다. 이 경우 JPQL이 과도하게 복잡해지거나 결국 네이티브 쿼리로 빠지게 되고, 일부 로직은 코드에서 재가공하게 되면서 유지보수가 어려워진다.

그렇다면 MyBatis를 사용하면 해결될까? SQL을 직접 작성할 수 있기 때문에 더 유리해 보이지만, 집계 문제의 본질은 그대로 남아 있다.

SELECT date, SUM(amount)
FROM orders
GROUP BY date;

 

이런 집계 쿼리는 결국 DB가 대량 데이터를 스캔해야 하기 때문에 CPU와 디스크 I/O 사용량이 증가한다. 트래픽이 붙으면 DB 전체 성능 저하로 이어지고, 다른 서비스까지 영향을 주게 된다.

또한 다음과 같은 방식도 많이 사용된다.

 

INSERT INTO summary_table
SELECT ...
FROM orders;

 

이 구조는 한 번에 많은 데이터를 처리하기 때문에 실행 시간이 길고 락을 오래 잡는다. 중간에 실패하면 전체 롤백이 발생하기 때문에 안정성도 떨어진다.

정리하면 JPA는 애플리케이션 메모리와 트랜잭션에서 문제가 발생하고, MyBatis는 DB 부하와 락에서 문제가 발생한다. 하지만 둘 다 공통적으로는 대량 데이터를 한 번에 처리하는 구조라는 동일한 한계를 가지고 있다.


해결 방법: 집계는 배치로 분리한다

이 문제를 해결하는 방법은 단순하다.
집계를 요청 시점에 계산하지 않고, 미리 계산해서 저장해두는 구조로 바꾸는 것이다.

Spring Batch를 사용하면 이 방식을 안정적으로 구현할 수 있다. 배치는 데이터를 한 번에 처리하지 않고 일정 단위(Chunk)로 나눠서 읽고, 처리하고, 저장한다. 이 과정에서 트랜잭션도 분리되기 때문에 메모리 사용량을 제어할 수 있고, 특정 구간에서 실패하더라도 전체를 다시 실행할 필요 없이 해당 구간만 재처리할 수 있다.

또한 집계 작업을 사용자 요청과 분리할 수 있기 때문에 DB 부하를 트래픽이 적은 시간대로 이동시킬 수 있다. 결국 핵심은 하나다.

 

집계는 실시간으로 계산하지 않고, 배치로 미리 만들어둔다

 


왜 집계를 일·주·월 단위로 나누는가

집계 배치를 설계할 때 대부분 일, 주, 월 단위로 나누는 이유는 실제 서비스의 조회 패턴과 맞기 때문이다. 사용자는 대부분 오늘, 이번 주, 이번 달 기준으로 데이터를 조회한다. 따라서 이 단위로 미리 데이터를 만들어두는 것이 가장 효율적이다.

또한 집계 구조를 단계적으로 가져갈 수 있다는 장점이 있다. 일반적으로는 원본 데이터를 기준으로 바로 월 집계를 만드는 것이 아니라, 먼저 일 단위 집계를 생성하고 이를 기반으로 주, 월 데이터를 만든다. 이렇게 하면 전체 데이터를 다시 계산할 필요 없이 이미 만들어진 데이터를 재활용할 수 있고, 성능과 안정성을 동시에 확보할 수 있다.

문제가 발생했을 때도 일 단위 데이터만 재생성하면 되기 때문에 운영 측면에서도 훨씬 유리하다.


이 문제를 해결하는 가장 현실적인 방법은 단순하다.
집계를 요청 시점에 계산하지 않고, 미리 계산해서 저장해두는 구조로 바꾸는 것이다.

Spring Batch를 사용하면 이 구조를 안정적으로 구현할 수 있다. 배치는 데이터를 한 번에 처리하지 않고 Chunk 단위로 나눠서 읽고, 처리하고, 저장한다. 이 과정에서 트랜잭션도 분리되기 때문에 메모리 사용량을 제어할 수 있고, 특정 구간에서 실패하더라도 전체를 다시 실행할 필요 없이 해당 구간만 재처리할 수 있다. 또한 집계 작업을 사용자 요청과 분리하기 때문에 DB 부하를 트래픽이 적은 시간대로 이동시킬 수 있다.

결국 핵심은 하나다.

집계는 실시간으로 계산하지 않고, 배치로 미리 만들어둔다

집계 테이블 설계와 배치 구조 예시

실무에서는 원본 데이터와 집계 데이터를 명확히 분리해서 관리한다.
예를 들어 주문 데이터를 기준으로 한다면 다음과 같은 구조를 사용한다.

orders

 

일 단위 집계 테이블은 날짜 기준으로 단순하게 가져간다.

 

CREATE TABLE daily_summary (
    summary_date DATE PRIMARY KEY,
    total_amount DECIMAL(15,2),
    order_count INT,
    created_at DATETIME
);

 

주 단위와 월 단위는 복합 키 형태로 관리한다.

 

CREATE TABLE weekly_summary (
    year INT,
    week INT,
    total_amount DECIMAL(15,2),
    order_count INT,
    PRIMARY KEY (year, week)
);

CREATE TABLE monthly_summary (
    year INT,
    month INT,
    total_amount DECIMAL(15,2),
    order_count INT,
    PRIMARY KEY (year, month)
);

 

배치 흐름은 다음과 같이 구성하는 것이 가장 안정적이다.

 

orders → (일 배치) → daily_summary  
daily_summary → (주 배치) → weekly_summary  
daily_summary → (월 배치) → monthly_summary

 

이 구조의 핵심은 상위 집계를 하위 집계 기반으로 생성하는 것이다.
중복 계산을 줄이고, 재처리 범위를 최소화할 수 있다.


결론

집계는 단순히 쿼리를 잘 작성한다고 해결되는 문제가 아니다.
JPA든 MyBatis든 대용량 데이터를 실시간으로 집계하려는 순간 구조적인 한계에 부딪힌다.

그래서 실무에서는 명확하게 역할을 나눈다.

  • 원본 데이터 처리 → JPA / MyBatis
  • 집계 데이터 생성 → Batch

그리고 집계는 미리 만들어두고, 조회는 가볍게 가져가는 구조로 설계한다.
이 방식이 결국 성능, 안정성, 확장성을 모두 확보하는 가장 현실적인 방법이다.

 

 

반응형