루비에서의 동시성 및 병렬성: 스레드, 파이버, 리액터 활용

Magesh, "Concurrency in Ruby: Threads, Fibers, and Ractors Demystified"

작성자
EuRuKo
발행일
2025년 02월 24일

핵심 요약

  • 1 루비는 스레드, 파이버, 리액터를 통해 동시성 및 병렬성을 지원하여 CPU 활용도를 높입니다.
  • 2 스레드와 파이버는 IO 집약적 작업에 적합한 동시성을 제공하며, 파이버는 경량으로 더 많은 동시 작업에 유리합니다.
  • 3 리액터는 CPU 집약적 작업에 진정한 병렬성을 제공하며, 격리된 메모리 모델로 경쟁 조건 문제를 해결합니다.

도입

현대 시스템에서 CPU의 발전은 눈부시지만, 단일 스레드 비동시성 코드로는 시스템 리소스의 25~30%만이 활용되는 경우가 많습니다. 이는 CPU의 잠재력을 충분히 활용하지 못하고 있음을 의미합니다. 이러한 비효율성을 극복하고 시스템 성능을 최적화하기 위해 동시성(Concurrency)과 병렬성(Parallelism)의 개념을 이해하고 루비(Ruby)에서 이를 효과적으로 구현하는 방법이 중요하게 부각됩니다. 본 발표는 루비가 제공하는 스레드(Threads), 파이버(Fibers), 그리고 리액터(Reactors)를 중심으로 동시성과 병렬성을 구현하는 다양한 접근 방식과 각 방식의 특장점, 그리고 주의사항을 심층적으로 다룹니다. 동시성은 여러 작업을 '다루는' 것에 가깝고, 병렬성은 여러 작업을 '동시에 수행하는' 것에 초점을 맞춥니다.

루비는 다양한 메커니즘을 통해 동시성과 병렬성을 지원합니다. 첫 번째는 스레드입니다. 프로세스에 비해 경량이며 메모리를 공유한다는 장점이 있습니다. 그러나 MRI(Matz’s Ruby Interpreter) 루비에서는 GIL(Global Interpreter Lock), 즉 전역 인터프리터 잠금으로 인해 진정한 병렬성(Parallelism)을 달성하기 어렵습니다. GIL은 한 번에 하나의 스레드만 실행되도록 허용하므로, 여러 스레드가 동시에 실행되는 것처럼 보이지만 실제로는 빠르게 컨텍스트 스위칭(Context Switching)하며 번갈아 실행되는 동시성(Concurrency)에 가깝습니다. IO(입출력) 집약적인 작업(예: 웹 스크래핑, API 호출)에서는 스레드를 통해 상당한 성능 향상(예: 2.5초에서 0.14초로 단축)을 얻을 수 있습니다. 하지만 스레드 사용 시에는 공유 상태로 인한 경쟁 조건(Race Condition)이나 교착 상태(Deadlock)와 같은 문제에 직면할 수 있으며, 이를 해결하기 위해 뮤텍스(Mutex) 잠금, 원자적(Atomic) 연산, 잠금 순서 정렬과 같은 동기화 기법이 필수적입니다. 또한, 운영체제 수준에서 발생하는 컨텍스트 스위칭 비용이 비교적 높다는 단점이 있습니다.

두 번째는 파이버입니다. 파이버는 코루틴(Coroutines)과 유사한 경량 스레드로 간주될 수 있습니다. 스레드와 달리 파이버의 컨텍스트 스위칭은 개발자(Fiber.yield, Fiber.resume)에 의해 명시적으로 제어됩니다. 이는 운영체제의 개입 없이 이루어지므로 스레드에 비해 컨텍스트 스위칭 비용이 훨씬 저렴하며, 수백, 수천 개의 파이버를 생성하는 것이 스레드를 생성하는 것보다 훨씬 효율적입니다. 파이버 역시 IO 집약적인 작업에서 뛰어난 성능 향상(예: 6초에서 3.8초로 단축)을 보여주며 동시성을 달성하는 데 효과적입니다. 루비 3.0부터는 스케줄러(Scheduler)를 통해 IO 대기 시 다른 파이버로 전환하는 등의 최적화가 가능해졌습니다. async gem과 같은 비동기 라이브러리를 활용하면 파이버를 보다 쉽게 관리할 수 있습니다. 하지만 파이버는 스레드와 마찬가지로 진정한 병렬성을 제공하지 않으며, 특정 파이버가 제어권을 양보하지 않으면 다른 파이버가 실행되지 못하는 스타베이션(Starvation) 문제가 발생할 수 있습니다.

세 번째는 리액터입니다. 리액터는 루비에서 진정한 병렬성을 달성하기 위한 옵션입니다. 이는 경량 프로세스와 유사하게 동작하며, 각 리액터는 독립적인 메모리 공간을 가집니다. 이 메모리 격리 덕분에 스레드에서 흔히 발생하는 경쟁 조건 문제를 원천적으로 방지할 수 있습니다. 리액터 간의 통신은 send, receive, yield, take와 같은 메시지 전달 메커니즘을 통해 이루어지며, 이때 전달되는 데이터는 반드시 불변(Immutable)해야 합니다. 리액터는 GIL의 영향을 받지 않으므로, 다수의 리액터가 여러 CPU 코어에서 동시에 실행되어 CPU 리소스를 효율적으로 활용할 수 있습니다. 이는 특히 CPU 집약적인 작업(예: 대용량 CSV 파일 처리 및 계산)에서 상당한 성능 향상(예: 4.7초에서 1.6초로 단축)을 가져옵니다. 현재 리액터는 실험적인 기능으로, 일부 기존 라이브러리와의 호환성 문제가 있을 수 있습니다.

결론

결론적으로, 루비에서 동시성과 병렬성을 구현하는 방식은 작업의 특성에 따라 신중하게 선택해야 합니다. 스레드와 파이버는 네트워크 호출이나 파일 입출력과 같은 IO 집약적인 작업에 적합하며, 시스템이 외부 응답을 기다리는 동안 다른 작업을 처리하여 효율성을 높입니다. 특히 파이버는 스레드보다 훨씬 경량이며 개발자 제어 컨텍스트 스위칭을 통해 대규모 동시 작업에 유리합니다. 반면, 리액터는 복잡한 계산이나 데이터 처리와 같은 CPU 집약적인 작업에 최적화되어 있으며, 진정한 병렬성을 통해 다중 코어 CPU의 성능을 최대한 끌어낼 수 있습니다. 각 도구의 장단점을 이해하고 적절히 활용한다면, 루비 애플리케이션의 성능을 2배에서 3배 이상 향상시켜 몇 시간 걸릴 작업을 몇 분으로 단축하는 등 놀라운 효율성을 달성할 수 있습니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!