-
findAll vs Stream vs Batch처리 비교개발 2024. 11. 7. 17:41
프로젝트에서 특정 경로에 대한 이벤트 발생 시 해당 경로를 일정 시간 내에 지나갔거나 근처에 있었던 모든 유저들에게 알림을 발송해야 합니다. 만약 알림을 보내야 하는 회원이 적다면 findAll()을 통해 전체 회원을 조회하여 처리해도 문제가 없겠지만, 서비스가 확장되어 알림 대상 회원 수가 급증할 경우에는 성능 및 메모리 사용 문제가 발생할 수 있습니다.
이에 따라, 데이터 양이 증가할 때 발생할 수 있는 문제를 분석하고, 여러 방식으로 성능 테스트를 진행해 보았습니다.
이번 테스트는 초기 1만 건의 데이터를 기준으로 시작하여, 힙 사이즈 512MB를 초과할 때까지 데이터 수를 2배씩 증가시키며 진행했습니다.
테스트 방식은 다음과 같습니다.
- 전체 회원 조회 후 한 건씩 알림 발송:
- 전체 데이터를 메모리에 한꺼번에 로드하기 때문에 메모리 사용량이 매우 큽니다.
- 데이터가 많을 경우 OutOfMemoryError가 발생할 수 있습니다.
- 배치 처리 방식:
- 데이터를 일정 크기로 나누어 처리하여 메모리 점유율이 상대적으로 적습니다.
- 그러나 배치 단위로 여러 번 조회하므로 데이터베이스 쿼리가 많아져 I/O 오버헤드가 발생할 수 있습니다.
- 커서 형태로 단건 처리:
- 전체 데이터를 메모리에 로드하지 않고 필요할 때마다 한 건씩 처리하므로 메모리 사용이 효율적입니다.
- 다만, 긴 커넥션 유지가 필요한 경우 커넥션 풀 설정을 적절히 조정해야 하며, 서비스 환경에 따라 커넥션 고갈 문제가 발생할 수 있습니다.
데이터 640,000 건
힙 사이즈는 초기 설정되어 있던 512MB 에서 진행했습니다.
@Test void testFindAll() { List<Notification> all = this.notificationRepository.findAll(); all.forEach(n -> n.notify()); } @Test void testFindAllStream() { Stream<Notification> allStream = this.notificationRepository.streamAllBy(); allStream.forEach(n -> { n.notify(); em.detach(n); // 엔티티 관리 해제 }); } @Test void testFindAllBatch() { int batchSize = 1000; // 배치 크기 설정 Integer lastId = null; // 마지막 조회한 ID를 저장할 변수 List<Notification> batch = this.notificationRepository.findFirstBatch(batchSize); while (!batch.isEmpty()) { for (Notification n : batch) { n.isConfirmed(); em.detach(n); // 엔티티 관리 해제 } // 마지막 조회된 항목의 ID 업데이트 lastId = batch.get(batch.size() - 1).getId(); em.clear(); // 메모리 절약을 위해 엔티티 매니저 클리어 // 마지막 ID 기준으로 다음 배치를 조회 batch = this.notificationRepository.findBatchAfterId(lastId, batchSize); } }
세가지 케이스를 작성한 뒤 실행 전 후로 로깅을 남겨 측정하였습니다.
5번씩 실행하였고 평균치를 표로 정리하였습니다.
테스트 방식 평균 메모리 사용량 실행 시간 오류 여부 findAll 512 MB 초과 X ms OutOfMemoryError 발생 Stream 291.1 MB 2272 ms 정상 실행 Batch 처리 151.2 MB 2960 ms 정상 실행
단일 스레드 환경에서 결과- 전체 회원 조회 (findAll):
- 전체 데이터를 메모리에 한꺼번에 로드하기 때문에 메모리 사용량이 매우 높습니다. 데이터가 많아지면 OutOfMemoryError가 발생할 수 있어 대규모 데이터에는 부적합합니다.
- 커서 기반의 단건 처리 (Stream):
- 메모리 사용을 효율적으로 관리하며, 메모리 사용량을 약 291MB로 유지합니다. 필요한 데이터만 건별로 처리하기 때문에 메모리 절약 효과가 있고 실행시간도 가장 빨랐습니다.
- 배치 처리 방식:
- 데이터를 일정한 크기로 나누어 처리해 메모리 사용량을 낮게 유지할 수 있습니다. 데이터베이스에 여러 번 접근하게 되지만, 메모리 절약 효과는 가장 뛰어났습니다. 다만, 배치 단위로 쿼리를 여러 번 실행하기 때문에 실행 시간이 비교적 길어질 수 있습니다.
이러한 결과로 볼 때, 대규모 데이터에 대해 메모리 효율성을 고려해야 할 경우 배치 처리 방식이 가장 적합하며, 실시간성을 고려한 메모리 효율적인 처리가 필요한 경우 커서 기반의 단건 처리 방식이 유용할 수 있습니다.
멀티 스레드 환경
위의 경우는 단일 스레드가 동기 방식으로 동작할 때의 경우이며 운영 환경에서는 멀티 스레드 환경에서 동시에 여러 스레드가 해당 작업을 실행하게 됩니다. 따라서 스프링의 TaskExecutor 를 사용하여 비동기적으로 여러 스레드가 작업을 호출하는 경우도 테스트 해보았습니다.
@Test void testStream() { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 50; i++) { executorService.execute(() -> notifyTest.processStream()); } executorService.shutdown(); } @Test void testBatchProcessing() { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 50; i++) { executorService.execute(() -> notifyTest.processBatch()); } executorService.shutdown(); }
멀티 스레드 환경 테스트 결과
테스트는 HikariCP의 최소 스레드 풀 크기인 10개의 고정된 스레드에서 각 작업을 50번씩 반복하여 수행했습니다. 힙 사이즈는 512MB로 설정하였고, 아래는 각 테스트 방식의 평균 메모리 사용량, 실행 시간, 및 오류 발생 여부입니다.
테스트 방식 평균 메모리 사용량 실행 시간 오류 여부 findAll 512 MB 초과 X ms OutOfMemoryError 발생 Stream 512 MB 초과 X ms OutOfMemoryError 발생 및 커넥션 고갈 Batch 처리 187 MB 24560 ms 정상 실행 - 전체 회원 조회 (findAll): 멀티 스레드 환경에서도 메모리를 초과하여 OutOfMemoryError가 발생했습니다. 대량의 데이터를 메모리에 로드하는 방식은 대규모 데이터 처리에 적합하지 않음을 재확인했습니다.
- 커서 기반 단건 처리 (Stream): 멀티 스레드 환경에서는 OutOfMemoryError와 함께 커넥션 풀 고갈 문제가 발생했습니다. "HikariPool-1 - Connection is not available" 로그가 빈번하게 발생하여, 커넥션이 부족하여 지연이 생겼습니다. 성공적으로 실행된 경우에도 평균 실행 시간이 약 12,566ms로, 비교적 높은 지연이 있었습니다.
- 배치 처리 (Batch 처리): 배치 방식은 평균 메모리 사용량을 187MB로 유지하며 안정적으로 실행되었습니다. 실행 시간은 약 24,560ms로 다른 방식보다 길지만, 안정적인 메모리 사용과 함께 커넥션 문제 없이 안정적으로 작동했습니다.
결론
멀티 스레드 환경에서 대규모 데이터를 처리할 때 배치 처리 방식이 가장 안정적이며, 메모리와 커넥션 리소스를 효율적으로 관리할 수 있는 방식으로 나타났습니다. 실시간 처리가 필요하다면, 스레드 풀의 크기나 커넥션 수를 적절히 조정하여 커서 기반 방식을 보완하거나 배치 크기를 최적화하는 추가적인 조정이 필요할 수 있습니다.
'개발' 카테고리의 다른 글
MySQL Binlog CDC 구현(1) - Binlog 읽기 (0) 2024.12.05 AOP를 활용하여 로깅 기능 개발 (1) 2024.11.13 JPA N+1 감지 기능 구현기 (0) 2024.10.30 JOOQ 테스트 환경 분리하기 (1) 2024.09.25 메인 기능, 부가 기능 분리 그리고 Transactional outbox pattern (2) 2024.09.04 - 전체 회원 조회 후 한 건씩 알림 발송: