과정을 즐기자

Goroutine 과 Java virtual thread 비교해보기 본문

Go

Goroutine 과 Java virtual thread 비교해보기

320Hwany 2024. 12. 20. 17:53

Java 21 LTS 버전부터 정식으로 Virtual Thread가 포함되었고 이전 글에서 알아본 적이 있습니다.

 

 

Java Virtual Thread 에 대해 알아보자

Java 21 LTS 버전부터 정식으로 Virtual Thread가 포함되었습니다.Virtual Thread 란 무엇일까요? 오라클 공식 문서에 있는 표현은 다음과 같습니다."Virtual threads are lightweight threads that reduce the effort of writing

320hwany.tistory.com

이번 글에서는 Java Virtual Thread와 비교해보면서 Golang에 있는 Goroutine에 대해 알아보겠습니다.

 

📕  Goroutine과 Virtual Thread

1. 경량 쓰레드 모델

Goroutine과 Java의 Virtual Thread 모두 OS 쓰레드에 직접 매핑되지 않는 경량 쓰레드입니다. 

2. 비동기 처리 지원

일반적으로 유저 쓰레드와 OS 쓰레드가 1대1로 매핑된다면 블로킹 작업이 있을 때 OS 쓰레드도 블로킹됩니다.

하지만 두 모델 모두 각각의 런타임(Goroutine은 Go 런타임, Virtual Thread는 JVM)에서 관리하여 

비동기 방식으로 처리할 수 있도록 도와줍니다.

3. 스케줄링 관리

두 모델 모두 컨텍스트 스위칭이 OS 레벨에서 일어나지 않습니다. 

Goroutine은 Go 런타임, Virtual Thread는 JVM 수준에서 스케줄링합니다.

 

즉 위와 같은 특징들로 인해 두 모델 모두 처리량이 높은 애플리케이션에서 높은 동시성 처리가 필요할 때 사용할 수 있습니다.

Goroutine은 Virtual Thread 보다 조금 더 세밀한 제어를 할 수 있습니다.

직접 예시를 만들어보면서 살펴보겠습니다.

📘  Goroutine 사용 예시

Hello World를 출력하는 함수를 만들고 이 함수를 10번 실행하도록 해보겠습니다.

이때 함수의 각 실행은 약 1초가 걸리도록 하였습니다.

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()

	for i := 0; i < 10; i++ {
		printHello()
	}

	elapsed := time.Since(start)
	fmt.Printf("걸린 시간: %s\n", elapsed)
}

func printHello() {
	fmt.Println("hello world")
	time.Sleep(time.Second)
}

 

걸린 시간: 10.010707292s 의 결과가 나왔습니다. 

main 함수를 실행하고 하나의 실행 흐름에서 printHello()를 순서대로 10번을 실행했기 때문입니다.

하지만 만약에 이 작업을 병렬로 실행해도 문제가 없는 경우라면 어떨까요? 

이때 goroutine을 사용해볼 수 있습니다.

package main

import (
    "fmt"
    "sync"
    "time"
)

var waitGroup sync.WaitGroup

func main() {
    start := time.Now()
    waitGroup.Add(10)

    for i := 0; i < 10; i++ {
       go printHelloWithGoroutine()
    }

    waitGroup.Wait()
    elapsed := time.Since(start)
    fmt.Printf("걸린 시간: %s\n", elapsed)
}

func printHelloWithGoroutine() {
    fmt.Println("hello world")
    time.Sleep(time.Second)
    waitGroup.Done()
}

 

걸린 시간: 1.001426875s 의 결과가 나왔습니다.

print 함수 앞에 go 키워드를 사용하였으며 waitGroup 을 사용하여 해당 작업이 모두 완료될 때까지 기다려주었습니다.

main 함수에서 시작된 쓰레드 흐름이 goroutine을 만나면 새로운 경량 쓰레드를 생성하여 해당 작업을 넘긴 것입니다.

따라서 10번의 함수 실행을 거의 동시에 병렬로 실행할 수 있게되어 약 10초 -> 약 1초로 성능을 향상 시킬 수 있습니다.

📗 Goroutine 사용시 주의할 점

이렇게 Goroutine은 경량 쓰레드로 동시 처리 성능을 향상 시킬 수 있다는 것을 확인하였습니다.

두 모델 모두 비슷한 메커니즘으로 동작하지만 Goroutine은 조금 더 세밀한 제어가 가능합니다.

 

하지만 Goroutine은 사용할 때 주의할 점이 있습니다. 단순하게 동시 처리 성능을 높인만큼 공유 자원에 대한

동시성 문제에 대해 고려해야 합니다. 한 가지 예시를 만들어보겠습니다.

 

1000번 동시에 1000원씩 입금을 한다면 최종적으로 100만원이라는 금액이 있어야 합니다

package main

import (
    "fmt"
    "sync"
)

var waitGroup1 sync.WaitGroup
var money1 = 0

func main() {
    waitGroup1.Add(1000)

    for i := 0; i < 1000; i++ {
       go deposit(1000)
    }

    waitGroup1.Wait()
    fmt.Println(money1)
}

func deposit(depositAmount int) {
    money1 += depositAmount
    waitGroup1.Done()
}

 

해당 로직을 실행한 결과 956000, 945000, 938000으로 100만원이 아닐 뿐만 아니라 매번 결과가 달랐습니다.

여러 경량 쓰레드가 동시에 money1 이라는 공유 자원에 접근하면서 문제가 발생한 것입니다.

 

뮤텍스를 사용하여 공유 자원에 접근할 때 문제가 발생하지 않도록 수정해보겠습니다.

package main

import (
    "fmt"
    "sync"
)

var waitGroup2 sync.WaitGroup
var money2 = 0
var lock = sync.Mutex{}

func main() {
    waitGroup2.Add(1000)

    for i := 0; i < 1000; i++ {
       go depositWithLock(1000)
    }

    waitGroup2.Wait()
    fmt.Println(money2)
}

func depositWithLock(depositAmount int) {
    lock.Lock()
    defer lock.Unlock()

    money2 += depositAmount
    waitGroup2.Done()
}

 

해당 로직을 실행한 결과 100만원이라는 정확한 결과가 나왔습니다.

📚  정리

이전에는 Java를 주로 했어서 Virtual Thread의 동작 원리에 대해서만 알고 있었는데 Golang을 배우기 시작하면서

Goroutine에 대해 알게되었습니다. 각각 언어마다 마주치는 문제도 비슷한 것 같고 해결하는 방법도 비슷하다는 것을 느꼈습니다.

경량 쓰레드, 비동기 처리, 스케줄링 방식 등등 다양한 면에서 비슷하다고 느꼈습니다.

 

하지만 Goroutine은 조금 더 세밀한 제어가 가능한 것 같고 Goroutine 간 메세지를 전달하는 메세지 큐 역할을 하는 채널이라는

개념도 있는 것 같아서 조금 더 학습한 뒤 다시 정리해보겠습니다.

 

예제 코드

 

GitHub - 320Hwany/goroutine-test

Contribute to 320Hwany/goroutine-test development by creating an account on GitHub.

github.com

 

'Go' 카테고리의 다른 글

Golang 에서의 예외 처리  (1) 2025.02.16