You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Coroutine에서 블로킹 코드를 사용하면 Dispatcher 전환이 필요하지만, 비동기 라이브러리를 사용하면 전환 없이 바로 사용할 수 있다.
// ❌ 잘못된 예 - Dispatchers.Default에서 블로킹suspendfunprocessData(): Data {
// Default Dispatcher의 스레드 고갈val result = blockingDatabaseCall() // 블로킹!return result
}
// ✅ 방법 1: withContext로 Dispatcher 전환 (블로킹 API 사용 시)suspendfunprocessData(): Data= withContext(Dispatchers.IO) {
// I/O Dispatcher에서 블로킹 작업 실행
jdbcTemplate.query("SELECT * FROM users") // 블로킹 JDBC
}
// ✅ 방법 2: 비동기 라이브러리 사용 (권장 - Dispatcher 전환 불필요)suspendfunprocessData(): Data {
// R2DBC, WebClient 등 논블로킹 API 사용// suspend 함수를 직접 지원하므로 Dispatcher 전환 불필요return r2dbcRepository.findById(id).awaitSingle()
}
// Virtual Thread / Coroutine 모두 주의 필요// ❌ ThreadLocal - 재사용되는 스레드에서 문제val threadLocal =ThreadLocal<User>()
suspendfunprocess() {
threadLocal.set(currentUser)
delay(100) // 다른 스레드에서 재개될 수 있음val user = threadLocal.get() // 다른 값이거나 null일 수 있음
}
// ✅ Coroutine - CoroutineContext 사용val userContext =object:CoroutineContext.Key<UserElement> {}
classUserElement(valuser:User) : CoroutineContext.Element {
overrideval key = userContext
}
suspendfunprocess() {
val user = coroutineContext[userContext]?.user
}
// ✅ Virtual Thread - ScopedValue (Java 21 Preview)private static finalScopedValue<User> CURRENT_USER=ScopedValue.newInstance();
void process() {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// 안전하게 user 접근 가능
});
}
4. 언제 무엇을 선택할까?
상황
추천
이유
기존 Java 블로킹 코드
Virtual Thread
코드 변경 최소화
새로운 Kotlin 프로젝트
Coroutine
더 세밀한 제어, 언어 통합
CPU 집약적 작업
일반 Thread
컨텍스트 스위칭이 오히려 오버헤드
매우 많은 동시 연결
Coroutine
메모리 효율 최고
간단한 웹 서버
Virtual Thread
설정만으로 적용 가능
실전 적용
성능 비교
// 10,000개 동시 작업 시뮬레이션// OS ThreadfunthreadTest() {
val executor =Executors.newFixedThreadPool(200)
val latch =CountDownLatch(10_000)
repeat(10_000) {
executor.submit {
Thread.sleep(100) // I/O 대기 시뮬레이션
latch.countDown()
}
}
latch.await() // 약 5초 (10000/200 * 100ms)
}
// Virtual Thread (Java 21)funvirtualThreadTest() {
val executor =Executors.newVirtualThreadPerTaskExecutor()
val latch =CountDownLatch(10_000)
repeat(10_000) {
executor.submit {
Thread.sleep(100) // 블로킹이지만 효율적
latch.countDown()
}
}
latch.await() // 약 120~200ms (스케줄링 오버헤드 포함)
}
// CoroutinefuncoroutineTest() = runBlocking {
val jobs =List(10_000) {
launch {
delay(100) // 논블로킹 대기
}
}
jobs.joinAll() // 약 105~150ms (가장 빠름)
}
메모리 사용량 비교
┌─────────────────────────────────────────────────────────────────────────┐
│ 10,000개 동시 작업 시 메모리 (단순 작업 기준) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ OS Thread │████████████████████████████████████│ ~10GB │
│ │ (1MB × 10,000, 고정 스택) │ │
│ │
│ Virtual Thread │████ │ ~50~100MB │
│ │ (초기 ~1KB, 호출 깊이에 따라 증가) │ │
│ │
│ Coroutine │██ │ ~10~20MB │
│ │ (수백 바이트~수 KB, 상태만 저장) │ ✅ 가장 효율적 │
│ │
│ * 호출 스택이 깊어지면 Virtual Thread 메모리 사용량 증가 │
│ * Coroutine은 CPS 변환으로 상태만 저장하여 메모리 효율 최고 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Spring Boot 적용
// Spring Boot 3.2+ with Virtual Thread// application.yml// spring:// threads:// virtual:// enabled: true// 기존 코드 그대로 사용 - 자동으로 Virtual Thread 적용
@Service
classUserService(
privatevaluserRepository:UserRepository
) {
funfindById(id:Long): User {
return userRepository.findById(id).orElseThrow()
}
}
// Spring WebFlux + Coroutine
@RestController
classUserController(
privatevaluserService:UserService
) {
@GetMapping("/users/{id}")
suspendfungetUser(@PathVariable id:Long): User {
return userService.findById(id)
}
@GetMapping("/users/{id}/dashboard")
suspendfungetDashboard(@PathVariable id:Long): Dashboard= coroutineScope {
val user = async { userService.findById(id) }
val orders = async { orderService.findByUserId(id) }
val recommendations = async { recommendationService.getFor(id) }
Dashboard(
user = user.await(),
orders = orders.await(),
recommendations = recommendations.await()
)
}
}
importkotlinx.coroutines.*importjava.util.concurrent.Executors// Virtual Thread 기반 Dispatcher 생성 (싱글톤으로 관리)object VirtualThreadDispatcher {
val dispatcher:CoroutineDispatcher by lazy {
Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher()
}
}
// 또는 확장 프로퍼티로 정의 (lazy로 한 번만 생성)
@OptIn(ExperimentalCoroutinesApi::class)
valDispatchers.VT:CoroutineDispatcher by lazy {
Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher()
}
실전 적용 예제
// 기존 방식: Dispatchers.IO 사용suspendfunfetchDataOld(): Data= withContext(Dispatchers.IO) {
// 기본 max(64, CPU코어수) 스레드 풀에서 실행 - 많은 동시 요청 시 병목
blockingHttpCall()
}
// 개선된 방식: Virtual Thread 사용suspendfunfetchDataNew(): Data= withContext(VirtualThreadDispatcher.dispatcher) {
// Virtual Thread에서 실행 - 블로킹해도 효율적
blockingHttpCall()
}
@Service
classOrderService(
privatevalorderRepository:OrderRepository,
privatevallegacyPaymentClient:LegacyPaymentClient, // 블로킹 APIprivatevalinventoryClient:InventoryClient// 블로킹 API
) {
privateval vtDispatcher =Executors.newVirtualThreadPerTaskExecutor()
.asCoroutineDispatcher()
suspendfunprocessOrder(request:OrderRequest): OrderResult= coroutineScope {
// 병렬 실행 + Virtual Thread로 블로킹 처리val payment = async(vtDispatcher) {
legacyPaymentClient.process(request.payment) // 블로킹 OK
}
val inventory = async(vtDispatcher) {
inventoryClient.reserve(request.items) // 블로킹 OK
}
// Coroutine의 구조화된 동시성 유지val paymentResult = payment.await()
val inventoryResult = inventory.await()
// 논블로킹 작업은 기본 Dispatcher 사용
orderRepository.save(Order.from(request, paymentResult, inventoryResult))
}
}