Virtual Thread ์‹œ๋ฆฌ์ฆˆ ๋ชฉ์ฐจ


๊ฐ™์€ ๋ชฉํ‘œ, ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•

Java 21์—์„œ Virtual Thread๊ฐ€ ์ •์‹์œผ๋กœ ๋“ค์–ด์˜จ ๋’ค โ€œ๊ทธ๋Ÿฌ๋ฉด Kotlin Coroutine์€ ํ•„์š” ์—†์–ด์ง€๋Š” ๊ฑด ์•„๋‹Œ๊ฐ€โ€๋ผ๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋‚˜์™”๋‹ค. ์—ฌ๊ธฐ์— Spring WebFlux(Project Reactor)๊นŒ์ง€ ๋”ํ•˜๋ฉด ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์„ธ ๊ฐ€์ง€ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ํ‘ธ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋œ๋‹ค.

  • Virtual Thread: JVM ๋Ÿฐํƒ€์ž„์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ. ๋ธ”๋กœํ‚น ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์ธ๋‹ค.
  • Kotlin Coroutine: ์–ธ์–ด ์ˆ˜์ค€์˜ ๊ธฐ๋Šฅ. suspend ํ•จ์ˆ˜์™€ ์ฝ”๋ฃจํ‹ด ๋นŒ๋”๋กœ ๋น„๋™๊ธฐ ํ๋ฆ„์„ ํ‘œํ˜„ํ•œ๋‹ค.
  • WebFlux (Reactor): ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ. Mono/Flux๋กœ ๋น„๋ธ”๋กœํ‚น ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•œ๋‹ค.

์ด ํŽธ์—์„œ๋Š” ๋‰ด์Šค๋ ˆํ„ฐ ๋ฐœ์†ก ์˜ˆ์‹œ๋ฅผ ์„ธ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๊ตฌ์กฐ์  ์ฐจ์ด๋ฅผ ์‚ดํŽด๋ณธ๋‹ค.


์ฝ”๋“œ ์Šคํƒ€์ผ ๋น„๊ต

Virtual Thread (Java)

public class NewsletterDispatcher {

    private final MailClient mailClient;

    public NewsletterDispatcher(MailClient mailClient) {
        this.mailClient = mailClient;
    }

    public void dispatch(List<Subscriber> subscribers, Newsletter newsletter) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (Subscriber subscriber : subscribers) {
                executor.submit(() -> mailClient.send(subscriber.email(), newsletter));
            }
        }
    }
}

mailClient.send()๋Š” ์ผ๋ฐ˜ ๋ธ”๋กœํ‚น ๋ฉ”์„œ๋“œ๋‹ค. ๊ธฐ์กด ๋ธ”๋กœํ‚น ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. Executor๋งŒ ๋ฐ”๊ฟจ๋‹ค.

Kotlin Coroutine

class NewsletterDispatcher(private val mailClient: MailClient) {

    suspend fun dispatch(subscribers: List<Subscriber>, newsletter: Newsletter) {
        coroutineScope {
            subscribers.forEach { subscriber ->
                launch(Dispatchers.IO) {
                    mailClient.send(subscriber.email, newsletter)
                }
            }
        }
    }
}

suspend ํ‚ค์›Œ๋“œ์™€ coroutineScope, launch ๊ฐ™์€ ์ฝ”๋ฃจํ‹ด ๋นŒ๋”๊ฐ€ ๋“ฑ์žฅํ•œ๋‹ค. mailClient.send()๊ฐ€ ๋ธ”๋กœํ‚น ํ˜ธ์ถœ์ด๋ผ๋ฉด Dispatchers.IO์—์„œ ์‹คํ–‰ํ•ด์•ผ ์Šค๋ ˆ๋“œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. mailClient.send()๊ฐ€ suspend ํ•จ์ˆ˜๋ผ๋ฉด Dispatchers.IO ์—†์ด ๊ธฐ๋ณธ ๋””์ŠคํŒจ์ฒ˜์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ณ , ์ง„์ •ํ•œ ๋น„๋ธ”๋กœํ‚น ์ฒ˜๋ฆฌ๊ฐ€ ๋œ๋‹ค.

WebFlux (Reactor)

public class NewsletterDispatcher {

    private final ReactiveMailClient mailClient;

    public NewsletterDispatcher(ReactiveMailClient mailClient) {
        this.mailClient = mailClient;
    }

    public Mono<Void> dispatch(List<Subscriber> subscribers, Newsletter newsletter) {
        return Flux.fromIterable(subscribers)
                .flatMap(subscriber -> mailClient.send(subscriber.email(), newsletter))
                .then();
    }
}

ReactiveMailClient.send()๋Š” Mono<Void>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ API๋‹ค. flatMap์œผ๋กœ ๊ฐ ๊ตฌ๋…์ž์—๊ฒŒ ๋น„๋™๊ธฐ ๋ฐœ์†ก์„ ์—ฐ๊ฒฐํ•˜๊ณ , ์ „์ฒด๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด then()์œผ๋กœ ๋งˆ๋ฌด๋ฆฌํ•œ๋‹ค. ๋ธ”๋กœํ‚น ์ฝ”๋“œ๊ฐ€ ์ „ํ˜€ ์—†๋‹ค. ๋Œ€์‹  API ์ „์ฒด๊ฐ€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์œผ๋กœ ๋ฐ”๋€๋‹ค.

flatMap์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ตœ๋Œ€ 256๊ฐœ ์ž‘์—…์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•œ๋‹ค(Queues.SMALL_BUFFER_SIZE). Virtual Thread์ฒ˜๋Ÿผ ํƒœ์Šคํฌ๋งˆ๋‹ค ์ฆ‰์‹œ ์‹œ์ž‘ํ•˜๋ ค๋ฉด flatMap(f, Integer.MAX_VALUE)๋กœ ๋™์‹œ์„ฑ ์ œํ•œ์„ ํ’€์–ด์•ผ ํ•œ๋‹ค. ๋‹ค๋งŒ ๊ธฐ๋ณธ๊ฐ’์€ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฐฑํ”„๋ ˆ์…” ์—ญํ• ๋„ ํ•˜๋ฏ€๋กœ ๋ฌด์ž‘์ • ์ œ๊ฑฐํ•  ํ•„์š”๋Š” ์—†๋‹ค.


๊ตฌ์กฐ์  ์ฐจ์ด

์„ธ ๋ฐฉ์‹์ด ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ๋จผ์ € ํ‘œ๋กœ ์ •๋ฆฌํ•˜๊ณ , ๊ฐ ํ•ญ๋ชฉ์„ ์ž์„ธํžˆ ์‚ดํŽด๋ณธ๋‹ค.

ํ•ญ๋ชฉ Virtual Thread Kotlin Coroutine WebFlux
์ค‘๋‹จ ๋ฐฉ์‹ JVM์ด I/O ์ง€์ ์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ suspend ํ˜ธ์ถœ ์ง€์ ์—์„œ ๋ช…์‹œ์  ์ค‘๋‹จ ์˜คํผ๋ ˆ์ดํ„ฐ ์ฒด์ธ, ์ค‘๋‹จ ๊ฐœ๋… ์—†์Œ
์ทจ์†Œ/ํƒ€์ž„์•„์›ƒ StructuredTaskScope (Preview) withTimeout ์ž๋™ ์ „ํŒŒ .timeout() ์˜คํผ๋ ˆ์ดํ„ฐ
์ปจํ…์ŠคํŠธ ์ „ํŒŒ ScopedValue (Preview) / ThreadLocal CoroutineContext ์ž๋™ ์ „ํŒŒ Reactor Context, ๋ณ„๋„ ๋ธŒ๋ฆฌ์ง€ ํ•„์š”
์ฝ”๋“œ ์Šคํƒ€์ผ ๊ธฐ์กด ๋ธ”๋กœํ‚น ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ suspend ํ•จ์ˆ˜ ๋„์ž… ์„ ์–ธํ˜• ๋ฆฌ์•กํ‹ฐ๋ธŒ ํŒŒ์ดํ”„๋ผ์ธ
ํ•™์Šต ๋น„์šฉ ๋‚ฎ์Œ ์ค‘๊ฐ„ ๋†’์Œ

์–ด๋””์„œ ์ค‘๋‹จ๋˜๋Š”๊ฐ€

Virtual Thread๋Š” ๋ธ”๋กœํ‚น I/O ์ง€์ ์—์„œ JVM์ด ์ž๋™์œผ๋กœ carrier๋ฅผ ์–‘๋ณดํ•œ๋‹ค. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ค‘๋‹จ ์ง€์ ์„ ๋ช…์‹œํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

Coroutine์€ suspend ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ง€์ ์—์„œ๋งŒ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๋‹ค. ์ค‘๋‹จ ๊ฐ€๋Šฅํ•œ ์ง€์ ์ด ์ฝ”๋“œ์— ๋ช…์‹œ์ ์œผ๋กœ ๋“œ๋Ÿฌ๋‚œ๋‹ค.

WebFlux๋Š” ์ค‘๋‹จ์ด๋ผ๋Š” ๊ฐœ๋…์ด ์—†๋‹ค. flatMap, map ๊ฐ™์€ ์˜คํผ๋ ˆ์ดํ„ฐ๊ฐ€ ๋น„๋ธ”๋กœํ‚น ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•˜๊ณ , Reactor ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ํ๋ฆ„์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ฝ”๋“œ ์ „์ฒด๊ฐ€ ์„ ์–ธํ˜•์œผ๋กœ ํ‘œํ˜„๋œ๋‹ค.

์ทจ์†Œ์™€ ํƒ€์ž„์•„์›ƒ

Coroutine์€ ๊ตฌ์กฐ์  ๋™์‹œ์„ฑ(Structured Concurrency)์„ ์ง€์›ํ•œ๋‹ค. coroutineScope๊ฐ€ ์ทจ์†Œ๋˜๋ฉด ๊ทธ ์•ˆ์˜ ๋ชจ๋“  launch ๋ธ”๋ก์ด ํ•จ๊ป˜ ์ทจ์†Œ๋œ๋‹ค.

withTimeout(5.seconds) {
    coroutineScope {
        subscribers.forEach { subscriber ->
            launch {
                mailClient.send(subscriber.email, newsletter)
            }
        }
    }
}

withTimeout์€ kotlin.time.Duration์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š”๋‹ค. 5์ดˆ ์•ˆ์— ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด coroutineScope ์ „์ฒด๊ฐ€ ์ทจ์†Œ๋˜๊ณ , ์ง„ํ–‰ ์ค‘์ธ ๋ชจ๋“  ์ž์‹ ์ฝ”๋ฃจํ‹ด์— ์ทจ์†Œ ์‹ ํ˜ธ๊ฐ€ ์ „ํŒŒ๋œ๋‹ค.

WebFlux๋Š” ์˜คํผ๋ ˆ์ดํ„ฐ ํ•œ ์ค„๋กœ ๋™์ผํ•œ ํšจ๊ณผ๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

dispatch(subscribers, newsletter)
    .timeout(Duration.ofSeconds(5))
    .subscribe();

Java์˜ Virtual Thread๋„ StructuredTaskScope(JEP 480, Java 23 Preview)๋ฅผ ํ†ตํ•ด ๊ตฌ์กฐ์  ๋™์‹œ์„ฑ์„ ์ง€์›ํ•œ๋‹ค. StructuredTaskScope๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ Virtual Thread๋ฅผ ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ์  ๋‹จ์œ„๋กœ ๋ฌถ์–ด ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    subscribers.forEach(subscriber ->
        scope.fork(() -> {
            mailClient.send(subscriber.email(), newsletter);
            return null;
        })
    );
    scope.join();
    scope.throwIfFailed();
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
}

ShutdownOnFailure ์ •์ฑ…์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž์‹ ์Šค๋ ˆ๋“œ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•  ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ฆ‰์‹œ ์ทจ์†Œํ•œ๋‹ค. join()์€ InterruptedException์„, throwIfFailed()๋Š” ExecutionException์„ ๋˜์ง€๋ฏ€๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์ปจํ…์ŠคํŠธ ์ „ํŒŒ

Virtual Thread๋Š” ThreadLocal ๊ธฐ๋ฐ˜ ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ž์‹ ์Šค๋ ˆ๋“œ๋กœ ์ „ํŒŒํ•˜๋ ค๋ฉด ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋ณต์‚ฌํ•ด์•ผ ํ•œ๋‹ค.

Java 21๋ถ€ํ„ฐ Preview๋กœ ๋„์ž…๋œ ScopedValue(JEP 481, Java 23 Preview)๋Š” ์ด ๋ฌธ์ œ๋ฅผ ๊ฐœ์„ ํ•œ๋‹ค. ๋ถˆ๋ณ€ ๊ฐ’์„ ํŠน์ • ๋ฒ”์œ„ ์•ˆ์—์„œ๋งŒ ์œ ํšจํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๋ฉฐ, StructuredTaskScope์™€ ํ•จ๊ป˜ ์“ฐ๋ฉด ์ž์‹ ์Šค๋ ˆ๋“œ๋กœ ์ž๋™ ์ƒ์†๋œ๋‹ค.

private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

ScopedValue.where(REQUEST_ID, "req-123").run(() -> {
    mailClient.send(subscriber.email(), newsletter);
});

๋‹ค๋งŒ ThreadLocal ๊ธฐ๋ฐ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(MDC, Spring Security ๋“ฑ)์™€์˜ ํ˜ธํ™˜์„ ์œ„ํ•ด์„œ๋Š” ์—ฌ์ „ํžˆ ๋ณ„๋„ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

Coroutine์€ CoroutineContext๋ฅผ ํ†ตํ•ด MDC ๊ฐ’, ํŠธ๋žœ์žญ์…˜ ์ปจํ…์ŠคํŠธ ๊ฐ™์€ ์ •๋ณด๋ฅผ ์ž์‹ ์ฝ”๋ฃจํ‹ด์— ์ž๋™์œผ๋กœ ์ „ํŒŒํ•  ์ˆ˜ ์žˆ๋‹ค. kotlinx-coroutines-slf4j๊ฐ€ ์ œ๊ณตํ•˜๋Š” MDCContext๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด MDC ๊ฐ’์ด ์ฝ”๋ฃจํ‹ด ์žฌ๊ฐœ ์‹œ ์ž๋™์œผ๋กœ ๋ณต์›๋œ๋‹ค.

launch(Dispatchers.IO + MDCContext()) {
    mailClient.send(subscriber.email, newsletter)
}

WebFlux๋Š” Reactor Context๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ „ํŒŒํ•œ๋‹ค. ํŒŒ์ดํ”„๋ผ์ธ ์•ˆ์—์„œ contextWrite()๋กœ ๊ฐ’์„ ์ฃผ์ž…ํ•˜๊ณ  Mono.deferContextual()๋กœ ์ฝ๋Š”๋‹ค.

Mono.just(subscriber)
    .flatMap(s -> mailClient.send(s.email(), newsletter))
    .contextWrite(Context.of("requestId", requestId));

ThreadLocal ๊ธฐ๋ฐ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” Reactor Context์™€ ์ง์ ‘ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•„ ๋ณ„๋„ ๋ธŒ๋ฆฌ์ง€ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.


์„ฑ๋Šฅ ๋น„๊ต

ํ•ญ๋ชฉ Virtual Thread Kotlin Coroutine WebFlux
์Šค๋ ˆ๋“œ ์ˆ˜ carrier ์ˆ˜ โ‰ˆ ์ฝ”์–ด ์ˆ˜ ๋””์ŠคํŒจ์ฒ˜ ์„ค์ •์— ๋”ฐ๋ผ ๋‹ค๋ฆ„ ์ด๋ฒคํŠธ ๋ฃจํ”„ (๋งค์šฐ ์ ์Œ)
I/O ๋Œ€๊ธฐ ์ฒ˜๋ฆฌ carrier ์–‘๋ณด suspend + ๋น„๋ธ”๋กœํ‚น ์„ ํƒ ๊ฐ€๋Šฅ ์™„์ „ ๋น„๋ธ”๋กœํ‚น
๊ทนํ•œ ๋™์‹œ ์—ฐ๊ฒฐ ๋ณดํ†ต ๋†’์Œ ๋งค์šฐ ๋†’์Œ
๋ฉ”๋ชจ๋ฆฌ (ํž™) ํž™ ์Šคํƒ, ํ•„์š”ํ•œ ๋งŒํผ๋งŒ continuation ๊ฐ์ฒด ์˜คํผ๋ ˆ์ดํ„ฐ ์ฒด์ธ

Virtual Thread๋Š” carrier ์ˆ˜๊ฐ€ CPU ์ฝ”์–ด ์ˆ˜์— ๋น„๋ก€ํ•œ๋‹ค. ๋Œ€๊ธฐ ์ค‘์—๋Š” carrier๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋ฒ„ ์›Œํฌ๋กœ๋“œ์—์„œ ์ถฉ๋ถ„ํ•œ ์„ฑ๋Šฅ์„ ๋‚ธ๋‹ค.

Coroutine์€ suspend + ๋น„๋ธ”๋กœํ‚น I/O ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์กฐํ•ฉํ•˜๋ฉด OS ์Šค๋ ˆ๋“œ๋ฅผ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ง„์ •ํ•œ ๋น„๋ธ”๋กœํ‚น ๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.

WebFlux๋Š” Netty์˜ ์ด๋ฒคํŠธ ๋ฃจํ”„ ์œ„์—์„œ ๋™์ž‘ํ•œ๋‹ค. ๊ทน์†Œ์ˆ˜์˜ ์Šค๋ ˆ๋“œ๋กœ ๋งค์šฐ ๋†’์€ ๋™์‹œ์„ฑ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด, ์—ฐ๊ฒฐ ์ˆ˜๊ฐ€ ๊ทน๋‹จ์ ์œผ๋กœ ๋งŽ์€ ํ™˜๊ฒฝ์—์„œ ์ด์ ์ด ์žˆ๋‹ค.

์ˆ˜์‹ญ๋งŒ ๊ฐœ ๋™์‹œ์„ฑ ์ƒํ™ฉ์ด ์•„๋‹ˆ๋ผ๋ฉด ์„ธ ๋ฐฉ์‹ ๊ฐ„ ๋ฉ”๋ชจ๋ฆฌ ์ฐจ์ด๋Š” ํฌ์ง€ ์•Š๋‹ค.


์„ ํƒ ๊ธฐ์ค€

์ƒํ™ฉ ๊ถŒ์žฅ
๊ธฐ์กด Java ๋ธ”๋กœํ‚น ์ฝ”๋“œ๋ฒ ์ด์Šค์— ๋น ๋ฅด๊ฒŒ ์ ์šฉ Virtual Thread
์ฝ”๋“œ ๋ณ€๊ฒฝ ์ตœ์†Œํ™” Virtual Thread
Kotlin ํ”„๋กœ์ ํŠธ Coroutine
๊ตฌ์กฐ์  ๋™์‹œ์„ฑ, ์ทจ์†Œ ์ „ํŒŒ๊ฐ€ ์ค‘์š”ํ•œ ๊ฒฝ์šฐ Coroutine
๋น„๋ธ”๋กœํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Ktor, R2DBC)์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ Coroutine
๊ทนํ•œ์˜ ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜, ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ƒํƒœ๊ณ„ ์ „์ฒด ํ™œ์šฉ WebFlux
Java + Kotlin ํ˜ผํ•ฉ ํ”„๋กœ์ ํŠธ ์ƒํ™ฉ์— ๋”ฐ๋ผ ํ˜ผํ•ฉ ๊ฐ€๋Šฅ

Virtual Thread๋Š” ๊ธฐ์กด Java ์ฝ”๋“œ๋ฒ ์ด์Šค์— ๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋…น์•„๋“ ๋‹ค. ๋ธ”๋กœํ‚น ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๊ณ  Executor๋งŒ ๋ฐ”๊พธ๋ฉด ๋˜๋ฏ€๋กœ, ๋ ˆ๊ฑฐ์‹œ ํ”„๋กœ์ ํŠธ๋‚˜ ํŒ€ ๋‚ด ๋น„๋™๊ธฐ ๊ฒฝํ—˜์ด ๋งŽ์ง€ ์•Š์€ ๊ฒฝ์šฐ์— ์‹ค์šฉ์ ์ธ ์„ ํƒ์ด๋‹ค.

Coroutine์€ Kotlin ํ”„๋กœ์ ํŠธ์—์„œ ์ œ ์—ญ๋Ÿ‰์„ ๋ฐœํœ˜ํ•œ๋‹ค. withTimeout, coroutineScope ๊ฐ™์€ ๊ตฌ์กฐ์  ๋™์‹œ์„ฑ ๋„๊ตฌ๊ฐ€ ์ทจ์†Œ ์ „ํŒŒ์™€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œํ˜„ํ•ด ์ค€๋‹ค. ๋น„๋ธ”๋กœํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Ktor ํด๋ผ์ด์–ธํŠธ, R2DBC)์™€ ์กฐํ•ฉํ•˜๋ฉด OS ์Šค๋ ˆ๋“œ๋ฅผ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ง„์ •ํ•œ ๋น„๋ธ”๋กœํ‚น ๊ฒฝ๋กœ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

WebFlux๋Š” ๊ทน๋‹จ์ ์ธ ๋™์‹œ ์—ฐ๊ฒฐ์ด ์š”๊ตฌ๋˜๋Š” ํ™˜๊ฒฝ, ๋˜๋Š” ์ด๋ฏธ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ƒํƒœ๊ณ„๋ฅผ ์ „๋ฉด ์ฑ„ํƒํ•œ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•˜๋‹ค. ํ•™์Šต ๋น„์šฉ๊ณผ ์ฝ”๋“œ ๋ณต์žก๋„๊ฐ€ ๋†’์€ ๋งŒํผ, ์ด ์ด์ ์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ์ƒํ™ฉ์ธ์ง€ ๋จผ์ € ๋”ฐ์ ธ๋ณด๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.


Virtual Thread + Coroutine

Kotlin ํ”„๋กœ์ ํŠธ์—์„œ Virtual Thread์™€ Coroutine์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค. Dispatchers.IO๋ฅผ Virtual Thread ๊ธฐ๋ฐ˜ Executor๋กœ ๊ต์ฒดํ•˜๋ฉด ๋‘ ๋ฐฉ์‹์„ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

val virtualThreadDispatcher = Executors.newVirtualThreadPerTaskExecutor()
    .asCoroutineDispatcher()

launch(virtualThreadDispatcher) {
    mailClient.send(subscriber.email, newsletter)
}

๋‹จ, ์ด ์กฐํ•ฉ์ด ํ•ญ์ƒ ๋” ๋‚˜์€ ์„ฑ๋Šฅ์„ ๋ณด์žฅํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค. ๋ธ”๋กœํ‚น ์ฝ”๋“œ๋ฅผ Dispatchers.IO์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ธฐ์กด ํŒจํ„ด์ด ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ถฉ๋ถ„ํ•˜๋‹ค. ๋‘ ๋ฐฉ์‹์„ ํ˜ผ์šฉํ•˜๊ธฐ ์ „์— ์‹ค์ œ๋กœ ๋ณ‘๋ชฉ์ด ์–ด๋””์— ์žˆ๋Š”์ง€ ๋จผ์ € ์ธก์ •ํ•˜๋Š” ๊ฒƒ์ด ์ˆœ์„œ๋‹ค.

WebFlux์™€ ํ˜ผ์šฉํ•˜์ง€ ์•Š๋Š” ์ด์œ 

WebFlux์™€ Virtual Thread๋Š” ํ•จ๊ป˜ ์“ฐ์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. WebFlux๋Š” ์†Œ์ˆ˜์˜ ์ด๋ฒคํŠธ ๋ฃจํ”„ ์Šค๋ ˆ๋“œ๊ฐ€ ๋น„๋ธ”๋กœํ‚น์œผ๋กœ ๋ชจ๋“  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋‹ค. ์—ฌ๊ธฐ์— Virtual Thread๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋‘ ์Šค๋ ˆ๋“œ ๋ชจ๋ธ์ด ์ถฉ๋Œํ•ด ์–ด๋А ์ชฝ์˜ ์ด์ ๋„ ์ œ๋Œ€๋กœ ์–ป์ง€ ๋ชปํ•œ๋‹ค.

Virtual Thread๋Š” spring-boot-starter-web ๊ธฐ๋ฐ˜ Servlet ์Šคํƒ์—์„œ ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ ํ•ฉํ•˜๋‹ค. WebFlux(spring-boot-starter-webflux) ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š๋Š”๋‹ค.


๋งˆ๋ฌด๋ฆฌ

Virtual Thread, Kotlin Coroutine, WebFlux๋Š” ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ์ˆ˜์ค€์—์„œ ํ•ด๊ฒฐํ•œ๋‹ค.

  • Virtual Thread: ๋ธ”๋กœํ‚น ์ฝ”๋“œ์˜ ๋น„์šฉ์„ ๋‚ฎ์ถ”๋Š” JVM ๋Ÿฐํƒ€์ž„ ์ตœ์ ํ™”. ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์ธ๋‹ค.
  • Coroutine: ๋น„๋™๊ธฐ ํ๋ฆ„์„ ๊ตฌ์กฐ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ์–ธ์–ด ์ถ”์ƒํ™”. ์ทจ์†Œ ์ „ํŒŒ์™€ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ•์ ์ด๋‹ค.
  • WebFlux: ๋ฆฌ์•กํ‹ฐ๋ธŒ ํŒŒ์ดํ”„๋ผ์ธ ์ „์ฒด๋ฅผ ๋น„๋ธ”๋กœํ‚น์œผ๋กœ ๊ตฌ์„ฑํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ. ๊ทนํ•œ์˜ ๋™์‹œ์„ฑ์ด ํ•„์š”ํ•  ๋•Œ ๊ฐ•์ ์ด ์žˆ๋‹ค.

Virtual Thread๊ฐ€ ๋‚˜์™”๋‹ค๊ณ  Coroutine์ด๋‚˜ WebFlux๊ฐ€ ํ•„์š” ์—†์–ด์ง€๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค. Java๋งŒ ์“ด๋‹ค๋ฉด Virtual Thread๊ฐ€ ๊ฐ€์žฅ ์‹ค์šฉ์ ์ธ ์ถœ๋ฐœ์ ์ด๋‹ค. Kotlin์„ ์“ด๋‹ค๋ฉด Coroutine์ด ์ œ๊ณตํ•˜๋Š” ๊ตฌ์กฐ์  ๋™์‹œ์„ฑ์€ Virtual Thread๋งŒ์œผ๋กœ๋Š” ์–ป๊ธฐ ์–ด๋ ต๋‹ค. ์—ฐ๊ฒฐ ์ˆ˜๊ฐ€ ๊ทน๋‹จ์ ์œผ๋กœ ๋งŽ๊ณ  ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ƒํƒœ๊ณ„๋ฅผ ์ด๋ฏธ ์“ฐ๊ณ  ์žˆ๋‹ค๋ฉด WebFlux๊ฐ€ ์—ฌ์ „ํžˆ ์œ ํšจํ•˜๋‹ค.

์ง€๊ธˆ๊นŒ์ง€ Virtual Thread์˜ ๋™์ž‘ ์›๋ฆฌ๋ถ€ํ„ฐ Pinning ์ง„๋‹จ, ์„ฑ๋Šฅ ์ธก์ •, Spring Boot ์ ์šฉ, ๊ทธ๋ฆฌ๊ณ  ๋‹ค๋ฅธ ๋ฐฉ์‹๊ณผ์˜ ๋น„๊ต๊นŒ์ง€ ๋‹ค๋ค„๋ดค๋‹ค. ๊ฒฐ๊ตญ ์–ด๋А ์ชฝ์ด ๋” ๋‚ซ๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ๊ณผ ํŒ€ ์ƒํ™ฉ์— ๋งž๋Š” ๊ฒƒ์„ ๊ณ ๋ฅด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.