최근 프로젝트에서 PostgreSQL 기반의 대용량 데이터를 한 번에 집계하려다가 shared memory segment 에러를 자주 겪었습니다. 처음 한 달 정도는 잘 동작했지만, 기간을 늘려 6개월·1년 단위로 조회하는 순간 DB 메모리 관련 에러가 발생했습니다. 운영 환경에서 서버 파라미터를 크게 건드리기도 부담스러워서, 결국 쿼리를 기간 단위로 쪼개어 조회하고 자바에서 합산하는 방식으로 우회했습니다.
아래는 제가 직접 적용했던 과정과 방법들입니다.
1. 문제 원인
- 장기간 데이터를 한 번에 조회 → 정렬/집계 단계에서 메모리 과부하
- PostgreSQL은 공유 메모리 한도를 넘으면 바로 오류 발생
- DB 설정(work_mem, shared_buffers)을 조정하면 해결 가능하긴 하지만, 운영 중인 환경에서는 조심스럽습니다
2. 해결 전략: 월 단위 청크 처리
처음에는 단일 쿼리로 해결하려고 했지만, 현실적으로 무리가 있었습니다. 그래서 월 단위로 기간을 나눠서 쿼리하고, Java에서 합산하는 방식을 택했습니다.
쿼리 조건 수정
<select id="getDataByPeriod" parameterType="map" resultType="java.util.HashMap">
SELECT category, COUNT(*) AS cnt
FROM sample_table
WHERE object_id = #{objectId}
AND event_time >= #{from}
AND event_time < #{to}
GROUP BY category
ORDER BY cnt DESC
</select>
Java에서 기간 쪼개기
public class Range<T> {
private final T start, end;
public Range(T start, T end) { this.start = start; this.end = end; }
public T getStart() { return start; }
public T getEnd() { return end; }
}
public static List<Range<OffsetDateTime>> monthlyChunks(OffsetDateTime from, OffsetDateTime to) {
List<Range<OffsetDateTime>> result = new ArrayList<>();
YearMonth startYm = YearMonth.from(from.toLocalDate());
YearMonth endYm = YearMonth.from(to.toLocalDate());
YearMonth ym = startYm;
while (!ym.isAfter(endYm)) {
LocalDate cs = ym.equals(startYm) ? from.toLocalDate() : ym.atDay(1);
LocalDate ce = ym.equals(endYm) ? to.toLocalDate() : ym.atEndOfMonth();
result.add(new Range<>(
cs.atStartOfDay().atOffset(from.getOffset()),
ce.plusDays(1).atStartOfDay().atOffset(from.getOffset())
));
ym = ym.plusMonths(1);
}
return result;
}
3. 순차 처리 vs 병렬 처리
청크 단위로 나눈 후 순차 처리부터 시작했습니다. 안정적이긴 했지만 속도가 아쉬워서, 이후 제한된 병렬 처리(2~4스레드)로 확장했습니다.
구분 | 순차 처리 | 병렬 처리 |
---|---|---|
안정성 | 매우 높음. DB 리소스 사용이 일정 | 상대적으로 낮음. 커넥션 풀·메모리 한계 고려 필요 |
속도 | 느릴 수 있음 | 빠름 (특히 월 수가 많을 때 효과) |
구현 난이도 | 단순 | ExecutorService 활용 필요 |
권장 상황 | 운영 안정성이 최우선일 때 | 일정 수준의 부하 허용 가능 + 속도 개선 필요할 때 |
제가 테스트한 결과, 커넥션 풀 크기가 10인 환경에서는 병렬 3~4가 가장 적당했습니다. 그 이상은 속도는 비슷한데 DB 부하만 올라갔습니다.
4. 추가 대안들
제가 고려하거나 테스트했던 다른 방법들도 공유합니다.
- DB 파라미터 튜닝
- work_mem을 크게 주면 해시·정렬 단계가 디스크로 안 넘어가서 성능이 좋아집니다.
- 다만 운영 환경에서 전역 설정을 바꾸는 건 부담이 크므로 쉽게 선택하진 못했습니다.
- 쿼리 자체 최적화
- date_trunc('month', event_time)을 활용해 DB에서 월 단위 집계를 바로 해버리면 애초에 월별 쿼리로 잘리는 효과를 볼 수 있습니다.
- 단, GROUP BY date_trunc(...)는 여전히 한 번에 많은 데이터를 훑으므로 shared memory 문제는 남을 수 있습니다.
- 커서 기반 스트리밍 처리
- JDBC fetchSize를 설정하면 한꺼번에 데이터를 가져오지 않고 스트리밍합니다.
- mybatis.configuration.default-fetch-size=1000 정도로 두면 JVM 메모리 부담을 줄일 수 있습니다.
- 주 단위 / 일 단위 청크 처리
- 데이터량이 특히 많은 경우 월 단위도 벅찰 수 있어, 주·일 단위로 더 잘게 쪼개는 방법도 있습니다.
5. 결론
- 기간 단위 청크 처리는 대용량 쿼리로 인한 메모리 에러를 피할 수 있는 가장 현실적인 방법이었습니다.
- 처음엔 순차 처리로 안정성을 확보한 뒤, 병렬 처리로 속도를 보완했습니다.
- 상황에 따라 DB 파라미터 튜닝이나 쿼리 최적화도 함께 고려하면 더 좋은 성능을 낼 수 있습니다.
반응형
'개발 (Development) > PostgreSQL' 카테고리의 다른 글
[PostgreSQL/TimescaleDB] 데이터 적재 시 발생하는 statement_timeout 및 row is too big 오류 해결 방법 (0) | 2025.08.10 |
---|---|
[PostgreSQL] View와 Materialized View의 차이점과 사용법 (0) | 2025.08.03 |
[PostgreSQL] 문자열을 timestamp with time zone으로 변환하는 방법 (0) | 2025.07.27 |
[PostgreSQL] MyBatis foreach + UNION ALL 쿼리의 성능 문제와 PostgreSQL 최적화 (1) | 2025.07.05 |
[PostgreSQL] PostgreSQL에서 threshold 값 이력 관리 및 최신값 조회 테이블 설계하기 (0) | 2025.06.28 |