기존 프로젝트에서 레거시 스케줄러 코드에 배치 형식을 적용한 코드를 공유한다.
레거시 스케줄러에서도 배치 형식을 적용되어 있었다(ex. 백만건 -> 1000건 단위 배치 작업)
1000건을 threadPoolExecutor(thread-size : 10)를 사용하여 작업을 처리 하였는데
[AS-IS]
for(File file : fileList){
threadPoolExecutor.submit(new BulkFile(file,....));
}
이렇게 submit이 1000번 호출되고 10개는 스레드 풀이 작업을 나머지 990개는 queue에 쌓였다(스레드가 하나씩 가져와서 작업).
스레드가 하나씩 가져와서 작업하는 방식은 context-switching이 자주 발생하고 불필요한 오버해드가 많이 발생해서 비효율적인 방식이다.
그래서 다음과 같이 개선하였다:
- 전체 파일 리스트를 threadSize 기준으로 나누어 각 스레드가 일정량의 작업을 연속적으로 처리하도록 변경
- Guava의 Lists.partition()을 활용해 배치 분할
- 각 배치는 하나의 쓰레드에서 순차적으로 처리
[TO-BE]
List<List<File>> batchs = Lists.partition(fileList, listSize/threadSize);
for(List<File> batch : batchs){
threadPoolExecutor.submit(()->{
for(File file : batch){
new BulkFile(file,...).run();
}
});
}
이렇게하면 1000개 작업을 100개씩 10개 배치로 나눠서 하나의 스레드가 100개씩 작업하게 된다.
이 방식의 장점은 다음과 같다:
- 각 스레드가 쉬지 않고 일정량의 작업을 처리하게 되어 스레드 리소스를 효율적으로 사용
- 작업 큐에 데이터가 쌓이지 않고 즉시 실행됨
- 전체적으로 오버헤드가 줄어들고 성능이 개선됨
성능
- 동일한 1,000건 작업 기준으로, 기존에는 약 1분 정도 걸리던 작업이 개선 후에는 30초 이하로 단축됨을 확인
주의할 점
- 하나의 배치에서 예외가 발생하면 해당 스레드의 모든 작업이 중단될 수 있으므로, 적절한 예외 처리 필수
- submit()에서 람다를 사용할 경우 run()을 명시적으로 호출해야 동작(new BulkFile(...).run();)
레거시 코드라고 해서 무조건 그대로 사용하는 것이 아니라, "왜 이렇게 되어 있을까?", "더 나은 방식은 없을까?" 를 끊임없이 고민하며 개선해나가는 것이 중요하다는 걸 다시금 느꼈던 경험이었다.
'개발 > Java' 카테고리의 다른 글
Spring ResponseEntity Stream 처리 관련 오류 (2) | 2025.07.25 |
---|---|
대용량 파일 관리 스케줄러 개발 정리 (0) | 2025.03.04 |
자바 기초1 (0) | 2021.03.30 |