TaskExecutor를 사용한 비동기처리와 86% 성능개선

김채원·2025년 6월 28일
post-thumbnail

스프링 배치를 통한 성능개선으로 18%정도를 개선 시켰지만
데이터 10만건 기준 48초라는 여전히 아쉬운 속도가 나오는 문제가 있었다

이를 해결하기 위해 비동기 처리를 해주자

TaskExecutor

TaskExecutor란?
스프링 프레임워크에서 비동기 작업을 실행하기 위한 인터페이스

역시나 공식문서를 확인해보자

우선 Executors는 JDK에서 스레드 풀 개념을 지칭하는 이름이다

TaskExecutor는 자바의 Executor 인터페이스와 동일하며
내부적으로는 스레드 풀을 사용하지만 단일 스레드 또는 동기 실행 방식도 가능하다

ThreadPoolTaskExecutor은 가장 자주 쓰이는 구현체로
내부적으로 스레드 풀을 구성해서 재사용 가능하게 한다고 한다

ThreadPoolTaskExecutor

우선 ThreadPoolTaskExecutor가 가장 자주 쓰이는 구현체라고 하는데
왜인지 이유를 알아보자

- 성능 최적화

요청마다 스레드를 새로 만드는 것이 아닌
기존에 존재하던 스레드를 재사용하기 때문에
스레드 생성,제거 오버헤드가 줄어들어 효율적이라고 한다

- 세밀한 설정 가능

corePoolSize, maxPoolSize등과 같은 옵션으로
병렬처리에 세부동작이 가능하다고 한다

- Spring과의 완벽한 통합

@Async, @EnableAsync 같은 비동기 어노테이션과 함께 쓰면
별도의 설정 없이도 비동기 구현이 매우 간단해진다고 한다

이제 ThreadPoolTaskExecutor을 내 프로젝트에 적용해보자

@Configuration
public class AsyncTaskExecutorConfig {

	@Bean
	public TaskExecutor notificationTaskExecutor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setCorePoolSize(4);
		executor.setMaxPoolSize(8);
		executor.setQueueCapacity(2000);
		executor.setThreadNamePrefix("notification-task-");
		executor.initialize();
		return executor;
	}
}

추가한 컨피그 클래스이다
필자는 ,, 인텔 8코어를 사용하는 맥북이기 때문에
풀 사이즈를 넉넉하게 주고 capacity도 넉넉하게 줘서
무조건 성능에만 집중하고 싶긴하지만
현실적으로 생각해서 풀사이즈 4에 최대 8까지 설정해두었다

@Bean
	public Step sendDailyQuestionStep(
		JobRepository jobRepository,
		PlatformTransactionManager transactionManager,
		JpaPagingItemReader<User> userReader,
		ItemProcessor<User, Notification> itemProcessor,
		JdbcBatchItemWriter<Notification> itemWriter,
		TaskExecutor notificationTaskExecutor
	) {
		return new StepBuilder("sendDailyQuestionStep", jobRepository)
			.<User, Notification>chunk(CHUNK_SIZE, transactionManager)
			.reader(userReader)
			.processor(itemProcessor)
			.writer(itemWriter)
			.taskExecutor(notificationTaskExecutor)
			.build();
	}

이후에는 그냥 기존에 작성해둔 writer에 적용시켜주기만 하면 된다

성능테스트

성능 테스트 기준은 유저 10만명 기준으로 잡았다

우선 초기 방식이다
findAll() + for문 순차 저장 모든 유저를 findAll()로 조회한 후
for문을 통해 Notification을 하나씩 DB에 저장하는 방식이다

트랜잭션 내에서 동기적으로 순차 저장되며 병렬 처리나 청크 최적화가 전혀 없는 구조이다

처리 시간: 평균 약 58초


이후 비동기를 적용한 방식이다

Spring Batch의 taskExecutor를 사용하여
멀티 스레드 환경에서 병렬로 청크를 처리하도록 설정했다

처리 시간: 평균 약 8초

비율로 하면 약 86%에 성능 개선이 이뤄졌다
정말 ,,,,,,, 눈물이 난다 ,,,,,

마무리

저번 배치를 이용한 성능테스트에서 18%의 성능개선을 해봤지만 그래도 48초는 너무너무 아쉽다는 생각을 했는데 웬걸 .. 비동기는 정말 좋은것이구나 .. 운이 좋게도 순서에 대한 생각과 동시성 처리를 따로 염두하지 않아도 되는 코드를 구현해서 금방할 수 있었다. 다음번에는 동시성에 대한 고민을 할 수 있는 주제를 만들어보면 좋을거같다. 이번 테스트로 자신감을 얻어 다음엔 기존 reader를 jdbc를 활용하도록 수정하는게 목표다.

profile
김채원 판교간다

0개의 댓글