면접 준비하면서 얻은 내용을 하나씩 정리하고자 한다.
우선 나는 비동기 방식을 해야지! 라는 생각만 하면서 CompletableFuture를 도입했다.
이후 내 이력서, 코드를 보면서 궁금증이 들었다. 다음 코드를 봐보자!
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
위 코드는 CompletableFuture에서 runAsync나 supplyAsync를 실행하면 동작한다.
그럼 이 함수가 뭔지 알아보자
useCommonPool이 true일 때 사용
공유 스레드 풀을 사용하여 작업을 처리
시스템 전체에서 재사용 가능한 스레드들을 관리
useCommonPool이 false일 때 사용
각 작업마다 새로운 스레드를 생성
어쨌든 스레드를 비동기 처리 로직마다 하나씩 할당해준다.
잘 설계된 시스템은 1번 방식으로 쓰레드를 재사용한다고 했을때 이런 가정이 떠올랐다.
CPU: 4GB RAM
스레드 당 메모리: 1MB
최대 생성 가능 스레드: ~4000개
문제 상황
동시에 10000개의 요청 유입
가용 스레드(4000개) < 요청 수(10000개)
나머지 6000개 요청은 대기 상태로 빠짐
물론
큐잉 시스템 도입 (Kafka, RabbitMQ 등)
로드 밸런싱
Circuit Breaker 패턴 적용
Back Pressure 구현
같은 문제 해결 방식도 있겠지만, 비동기 처리 방법 중 다른 적절한 방법이 있지 않을까에 대한 의구심이 들었다.
그렇기 위해선 비동기 처리 방식이 뭐가 있는지를 알아야했다.
그런데 나는 리액티브 프로그래밍으로 된 것도 그렇고 Virual Thread를 처음들어봤기에 이 기술과 비교하려고 했다. 나중에 WebFlux를 심도있게 다루면 도움이 될 것 같다. Netty랑 같이
ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
전통적 스레드는 OS 수준의 네이티브 스레드와 1:1로 매핑된다. 각 스레드는
Thread virtualThread = Thread.startVirtualThread(() -> {
// task
});
가상 스레드는 JVM 수준에서 관리되는 경량 스레드이다
[10개 파일 업로드]
전통방식: 446ms
가상스레드: 325ms
[50개 파일 업로드]
전통방식: 1369ms
가상스레드: 326ms
[100개 파일 업로드]
전통방식: 2627ms
가상스레드: 316ms
// 전통적 방식
ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
// 가상 스레드 방식
Thread.startVirtualThread(() -> {
// I/O 작업 수행 시
// 다른 가상 스레드로 즉시 전환
});
[4개 작업]
전통방식: 43ms
가상스레드: 31ms
[8개 작업]
전통방식: 34ms
가상스레드: 32ms
[16개 작업]
전통방식: 56ms
가상스레드: 79ms
private static long performCpuIntensiveTask(int number) {
// 피보나치 계산
long result = calculateFibonacci(number);
// 소수 판별
for (int i = 2; i < 100000; i++) {
isPrime(i);
}
return result;
}
// 가상 스레드는 작업당 새로운 스레드 생성
for (int i = 0; i < taskCount; i++) {
Thread virtualThread = Thread.startVirtualThread(() -> {
performCpuIntensiveTask(i);
});
}
CompletableFuture<Void> future = CompletableFuture.runAsync(
task,
executorService
);
Thread virtualThread = Thread.startVirtualThread(() -> {
task.run();
});
가상 스레드에 적합한 경우
전통적 스레드에 적합한 경우
작업의 특성 파악
시스템 리소스
이러한 특성을 고려하여 적절한 스레드 처리 방식을 선택하면, 애플리케이션의 전반적인 성능을 최적화할 수 있다!!!!!
설명이 좋네요~