Coroutine vs Virtual Thread(1부)

차_현·2024년 12월 5일
1

코루틴과 Virtual Thread 비교와 사용 | 카카오페이 기술 블로그

위 글에 대한 샘플 코드 작성과, 내용 이해를 기반으로 한 정리를 해볼 예정이다.

요즘, 작년 전공 시간에 배운 동시성 및 락에 대해서 이론적으로 배우긴 하였지만, 실제 개발 환경에서 이를 적용해본 적은 거의 없었던 것 같다. 최근 동시성 이슈와 이를 해결하기 위한 다양한 방법들에 관심이 많아 실제 샘플, 예시 프로젝트 들을 진행해보며 진행하고 있는 프로젝트의 프로덕션 코드에 적용해볼 예정이다.

동시성을 처리하는 것은 매우 중요하고, 사용자의 유무와는 상관없이 유념해야할 부분이라고 생각한다. 동시성 처리를 함으로써, API Latency를 줄일 수 있고, 시스템 자원을 효율적으로 사용하는데 큰 이점을 가져다 준다. 또한, 네트워크 지연이나 대용량 데이터를 처리할 때, 다른 태스크가 중단되지 않고, 효율적인 방식으로 처리될 수 있게 해준다. 그럼 먼저 동시성 처리를 고려하지 않았을 때, 기존 방식의 문제점에 대해서 먼저 알아보자.

전통적인 방식의 문제점

전통적으로 동시성 처리는 운영체제의 스레드를 통해 이루어지며, 각 스레드는 독립적으로 작업을 수행하며 운영체제가 이들을 스케줄링하여 CPU에서 실행한다. 그러나 이러한 방식은 몇 가지 단점이 존재한다.

  • 고비용의 스레드 생성과 관리: 많은 스레드를 생성하고 관리하는 것은 비용이 많이 든다.
  • 복잡한 동기화 처리: 여러 스레드가 자원을 공유할 때 동기화 문제가 발생하며 이로 인해 코드가 복잡해진다.
  • 한정된 스레드 풀: 시스템에서 사용할 수 있는 스레드의 수가 한정적이어서 많은 동시 요청을 처리하기 어렵다.
  • I/O 대기 문제: 블로킹 I/O 작업으로 인해 스레드가 대기 상태에 있을 때, 다른 작업이 진행되지 않아 성능 저하가 발생한다.

코루틴(Coroutines)

코루틴은 비동기 작업을 더 효율적이고 직관적으로 작성할 수 있도록 돕는 코틀린의 기능이다. 코루틴의 핵심은 suspend라는 개념으로, 코루틴은 실행 중 특정 시점에서 일시 중단(suspend)되고, 필요할 때 다시 재개(resume)될 수 있다.

  1. 작업의 경량성

    코루틴은 실제로 스레드를 생성하지 않고, 하나의 스레드에서 여러 코루틴이 동작한다. 예를 들어, 블로킹 I/O 작업이나 네트워크 요청을 처리하는 동안 스레드 자원을 점유하지 않고 작업을 대기 상태로 전환하기 때문에 자원을 효율적으로 사용한다.

  2. I/O 작업에 강점

    코루틴은 네트워크 통신, 파일 읽기/쓰기 같은 작업에서 효율적으로 동작한다. 작업이 끝날 때까지 스레드가 대기하지 않고, 다른 작업을 수행하도록 양보할 수 있기 때문이다.

  3. 협력적 멀티태스킹

    코루틴은 협력적으로 동작한다. 코루틴이 실행되다가 suspend 함수가 호출되면, 스레드 점유를 자발적으로 다른 작업에 넘긴다. 이는 운영체제가 스케줄링을 강제로 조정하는 기존 스레드 방식보다 더 가볍고 유연하다.

Virtual Thread

Virtual Thread는 자바가 제공하는 경량화된 스레드 구현으로, Project Loom의 결과물이다. 기존의 운영체제 기반 스레드 방식의 단점을 해결하기 위해 설계되었다.

  1. 기존 스레드 모델을 경량화

    Virtual Thread는 운영체제 스레드와 1:1로 매핑되지 않는다. 대신 JVM 내부에서 관리되며, 하나의 운영체제 스레드에서 수십만 개의 Virtual Thread가 동작할 수 있다. 이는 대규모 동시성을 요구하는 애플리케이션에 적합하다.

  2. I/O 작업의 효율성

    Virtual Thread는 I/O 작업이 발생하면 효율적으로 대기 상태로 전환된다. 이를 통해 CPU 자원을 낭비하지 않고, 다른 작업이 원활히 수행되도록 한다.

  3. 기존 코드와의 호환성

    Virtual Thread는 기존 자바의 Thread API와 동일한 방식으로 사용할 수 있다. 따라서 기존에 작성된 자바 코드를 크게 수정하지 않아도 Virtual Thread로 전환할 수 있다. 이는 도입 장벽을 낮추는 큰 장점이다.

코루틴Virtual Thread
작동원리코루틴은 협력적 멀티태스킹 기반으로 동작하며, 스레드와는 독립적으로 실행된다.Virtual Thread는 운영체제 스레드 위에 경량화된 스레드를 추가한 구조로, 내부적으로 스레드처럼 동작한다.
사용환경코루틴은 코틀린 언어 기반으로 비동기 작업이 많은 상황(특히 I/O 작업)에 최적화되어 있다.Virtual Thread는 자바에서 동시성 처리를 개선하기 위해 설계되었으며, 대규모 동시성 작업에서 효과적이다.
언어 및 도구코루틴은 코틀린에서 제공하며, 코루틴 스코프와 같은 전용 API를 사용한다.Virtual Thread는 자바 21 이상에서 기본 Thread API와 동일한 방식으로 구현된다.

협력적 멀티 태스킹?

본격적으로 코드 예시로 위의 차이를 알아보기전에, 코틀린의 특징에서 설명되었던, 협력적 멀티 태스킹이라는 말이 크게 와닿지 않았다. 그래서 ‘협력적 멀티 태스킹’ 이라는 것이 도대체 어떤 의미인지 파악하고자 했다.

코루틴은 협력적 멀티태스킹 기반으로 동작하며, 스레드와는 독립적으로 실행된다.

위의 문장에서 ‘협력적 멀티태스킹’ 이라는 구문과 ‘스레드와 독립적으로 실행’ 이라는 구문이 눈에 띈다.

[협력적 멀티태스킹]

협력적 멀티태스킹은 코루틴이 스스로 실행을 중단하고(suspend), 다른 작업에 CPU를 넘길 수 있는 방식을 말한다. 운영체제의 스레드는 강제로 실행을 중단하고, 다른 스레드로의 전환이 이루어지지만, 코루틴은 협력적으로 양보한다.

[스레드와 코루틴의 차이]

  • 스레드는 운영체제가 제어하며, 강제로 중단되거나 스케줄링된다.
  • 코루틴은 코루틴 라이브러리가 관리하며, suspend라는 명시적인 호출로 중단과 재개를 제어한다

[스레드와 독립적으로 실행된다]

코루틴은 스레드 위에서 동작하긴 하지만, 스레드와 1:1 로 매핑되지 않고 여러 코루틴이 하나의 스레드에서 실행될 수 있다. 즉, 하나의 스레드에서 수십 개, 수백 개의 코루틴이 실행될 수 있다는 것이다. 코루틴은 스레드처럼 운영체제 스케줄링에 의존하지 않고, 내부적으로 코루틴 라이브러리가 자체적으로 실행 순서를 관리한다. 그렇기 때문에 결과적으로, 코루틴은 스레드보다 훨씬 가볍고 효율적으로 동시 작업을 처리할 수 있다.

[Java Thread 기반 코드]

public class ThreadTest {

  @Test
  public void testThreads() throws InterruptedException {
    Thread thread1 = new Thread(() -> {
      for (int i = 0; i < 3; i++) {
        System.out.println("스레드 1 작업 중");
        try {
          Thread.sleep(1000); // 블로킹 작업
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    Thread thread2 = new Thread(() -> {
      for (int i = 0; i < 3; i++) {
        System.out.println("스레드 2 작업 중");
        try {
          Thread.sleep(1000); // 블로킹 작업
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    thread1.start();
    thread2.start();

    // 두 스레드가 작업을 끝낼 때까지 대기
    thread1.join();
    thread2.join();

    System.out.println("테스트 완료!");
  }
}
  • 운영체제의 스레드는 CPU가 강제로 "스레드 1, 2 중 누구를 실행할지" 스케줄링한다.
  • 블로킹(Thread.sleep) 동안에도 스레드는 운영체제 자원을 점유하므로, 효율이 떨어질 수 있다.

[Kotlin Coroutine 기반 코드]

import kotlinx.coroutines.*
import kotlin.test.Test

class CoroutineTest {

    @Test
    fun testCoroutines() = runBlocking {
        val job1 = launch {
            repeat(3) {
                println("코루틴 1 작업 중")
                delay(1000) // 비블로킹 suspend 함수
            }
        }

        val job2 = launch {
            repeat(3) {
                println("코루틴 2 작업 중")
                delay(1000) // 비블로킹 suspend 함수
            }
        }

        job1.join() // job1이 끝날 때까지 대기
        job2.join() // job2가 끝날 때까지 대기

        println("테스트 완료!")
    }
}
  • 코루틴 생성 및 실행 launch를 사용하여 두 개의 코루틴(job1, job2)을 생성한다. 각 코루틴은 1초 간격으로 작업을 3번 반복한다.
  • 비블로킹 작업 delay() 는 비블로킹 함수로, delay(1000)은 코루틴을 1초 동안 일시 중단(suspend)시킨다. 하지만 스레드 자원을 점유하지 않으므로 다른 코루틴이 실행될 수 있다.
  • 협력적 멀티태스킹 코루틴은 실행 중에 스스로 suspend하여 다른 작업에 CPU를 양보한다. 이는 자원을 효율적으로 사용하는 협력적 멀티태스킹 방식이다.
  • 결과 코루틴 1과 코루틴 2가 번갈아가며 실행된다. 블로킹 작업이 없기 때문에 스레드 자원이 낭비되지 않는다.

이제 다음 포스팅에서, 이어서 코루틴(Coroutine)과 Virtual-Thread를 샘플 코드로 비교해보겠다.

0개의 댓글

관련 채용 정보