멤버 10명, 질문 100개, 답변 100개에 대해 쿼리 개수가 쌩으로 거의 10000개 가까이 날아가고 있죠
처리 시간도 2688ms로 아주 처참한 성능을 보이고 있습니다
사실 뭐 아주 긴 건 아닌데 그냥 뭐 그렇습니다
데이터가 더 많아지면 더 처참해질지도 모르겠네요
그래서 이전 게시글에서 알아봤던 대로 1:N에는 패치 조인 적용, 그리고 1:1 관계에는 추후 1:N으로 발전될 가능성이 없다 생각하여 FK를 변경해줬습니다
그리고 여기서 핵심 포인트
모든 처리가
@Scheduled(cron = "0 0 2 * * ?") // 매일 새벽 2시
private void validateQuestionCounts() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
int actualCount = memberRepository.countQuestionsByMemberId(member.getId());
if (member.getQuestionCount() != actualCount) {
member.updateQuestionCount(actualCount);
memberRepository.save(member);
}
}
}
동기 방식으로 순차적으로 진행되고 있었습니다
비록 새벽 시간대에 진행되지만 이러면 메인 스레드를 오랫동안 점유할 뿐만 아니라 전체적으로 작업이 지연되어 데이터가 많아지면 굉장히 비효율적인 작업이 될 것 같습니다
그래서 CompletableFuture를 사용하여 병렬 비동기 처리를 적용해보았습니다
CompletableFuture는 Java 8부터 도입된 복잡한 비동기 작업을 선언적으로 구성할 수 있도록 돕는 도구로, CompletableFuture 를 이용하면, 비동기 작업의 실행, 비동기 작업 간의 연결, 예외 처리, 결과 집계를 쉽게 할 수 있습니다
자 그럼 일단
@Scheduled(cron = "0 0 2 * * ?") // 매일 새벽 2시
public void validateQuestionCounts() {
List<Member> members = memberService.findAllMembers();
List<CompletableFuture<Void>> futures =
members.stream()
.map(memberService::validateQuestionCount)
.toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
}
CompletableFuture로 병렬 처리를 도입하고
@Async
@Transactional
public CompletableFuture<Void> validateQuestionCount(Member member) {
int actualCount = memberRepository.countQuestionsByMemberId(member.getId());
if (member.getQuestionCount() != actualCount) {
member.updateQuestionCount(actualCount);
memberRepository.save(member);
}
return CompletableFuture.completedFuture(null);
}
@Async로 비동기를 적용해주었습니다
이때 꼭 해줘야 하는게 있죠?
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "validationExecutor")
public Executor burstExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(0); // 평소에는 적은 수의 스레드 유지
executor.setMaxPoolSize(40); // 트래픽 급증 시 빠르게 확장
executor.setQueueCapacity(5); // 대기열을 작게 유지하여 빠르게 스레드 확장
executor.setThreadNamePrefix("Burst-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 과부하 시 호출 스레드에서 실행
executor.initialize();
return executor;
}
}
@EnableAsync를 해줬고요, 그리고 스레드 할당 전략을 만들어줬습니다
현재 저 작업은 하루에 한번 발생하는 일이기 때문에 평소에는 0개의 스레드를 유지하다가 조정 작업이 시작될 시 빠르게 확장할 수 있도록 대기열을 작게 유지하였습니다
이걸
@Async(value = "validationExecutor")
@Transactional
public CompletableFuture<Void> validateQuestionCount(Member member) {
int actualCount = memberRepository.countQuestionsByMemberId(member.getId());
if (member.getQuestionCount() != actualCount) {
member.updateQuestionCount(actualCount);
memberRepository.save(member);
}
return CompletableFuture.completedFuture(null);
}
@Async에 달아주면 적용 끝입니다
자 그럼 성능 측정해봐야죠?
실행 쿼리수가 굉장히 많이 줄어든 모습을 볼 수 있습니다
N+1 문제를 잘 정리해서 실행된 쿼리가 확 줄어든 모습이구요
총 실행 시간도 257ms로 약 90% 정도 줄어들었습니다
이렇게 N+1 문제 해결로 실행 쿼리 수를 줄이고 비동기 병렬 처리로 실행시간을 줄여봤습니다