멀티스레드 Ruby 코드 테스트: 불안정한 테스트를 해결하는 두 가지 전략

[Euruko 2025] “Don’t Be ‘Thread’-ened: Testing Multithreaded Code with Confidence” – Julia Egorova

작성자
EuRuKo
발행일
2025년 11월 10일

핵심 요약

  • 1 멀티스레드 Ruby 코드의 불안정한 테스트(flaky tests) 문제를 `sleep` 대신 '분할 정복'과 '스레드 동기화' 기법으로 해결하는 방법을 제시합니다.
  • 2 핵심 비즈니스 로직을 스레드 실행 로직과 분리하여 단일 스레드 환경에서 테스트하는 '분할 정복' 전략을 설명합니다.
  • 3 Ruby의 `concurrent-ruby` Gem에 포함된 `CountdownLatch`를 활용하여 스레드 간의 예측 가능한 동기화를 통해 안정적인 테스트를 구현하는 방법을 소개합니다.

도입

발표자는 비자 발급 과정의 불확실성에 비유하며, `sleep` 함수 사용으로 인해 지속적으로 CI 환경에서 테스트가 불규칙하게 실패하는(flaky test) 문제점을 제기합니다. 특히 NATS Ruby 클라이언트 개발 경험을 바탕으로 멀티스레드 Ruby 코드에서 이러한 문제가 더욱 심화되며, `sleep` 값 조정이나 반복적인 재실행이 근본적인 해결책이 될 수 없음을 지적합니다. 이 강연은 이러한 불안정한 테스트를 극복하고 안정적인 테스트 환경을 구축하기 위한 실용적인 접근 방식을 탐구합니다.

멀티스레드 코드의 불안정한 테스트를 개선하기 위해 두 가지 주요 전략이 제시됩니다. 이는 개발자가 테스트를 더 잘 제어하고 예측 불가능한 요소를 줄이는 데 중점을 둡니다.

1. 분할 정복 (Divide and Conquer)

  • 개념: 핵심 비즈니스 로직을 스레드 실행 로직과 분리하여 독립적인 메서드로 추출하는 기법입니다. 예를 들어, 무한 루프 내에서 반복적으로 호출되는 check_condition과 같은 핵심 작업을 별도의 메서드로 분리합니다.

  • 장점:

    • 추출된 핵심 로직은 더 이상 스레드나 무한 루프에 의존하지 않는 순수한 Ruby 코드가 됩니다.
    • 단일 스레드 환경에서 쉽게 테스트할 수 있으며, 기존 sleep 기반의 불안정한 테스트를 제거할 수 있습니다.
    • 이 방식만으로도 대부분의 테스트 요구사항(약 80% 이상)을 해결할 수 있어 효율적입니다.

2. 스레드 동기화 (Dealing with Threads Themselves)

  • 필요성: 핵심 로직이 분리된 후에도, 스레드가 특정 작업을 연속적으로 수행하는지 확인하는 등 스레드 자체의 동작을 테스트해야 하는 나머지 20%의 상황이 발생합니다. 이때 sleep은 여전히 부적절하며, 예측 불가능한 대기 시간을 초래합니다.

  • 대안: 스레드 간의 명확한 통신 및 동기화 메커니즘을 사용합니다.
    • Ruby Condition Variables: Ruby는 ConditionVariable을 통해 한 스레드가 특정 작업을 완료했음을 다른 스레드에 알리는 메커니즘을 제공합니다. 이는 Mutex와 함께 사용되지만, 코드가 복잡하고 보일러플레이트가 많아 테스트에 직접 적용하기에는 번거롭습니다.
    • Concurrent Ruby Gem의 CountdownLatch: concurrent-ruby는 Ruby를 위한 동시성 툴킷으로, CountdownLatchConditionVariable을 기반으로 하지만 훨씬 간결하고 사용하기 쉽습니다.
      • 작동 방식: 특정 카운트(기대하는 이벤트 발생 횟수)를 설정하고, 스레드 내에서 각 이벤트가 발생할 때마다 countdown 메서드를 호출하여 카운트를 감소시킵니다. 카운트가 0이 되면 대기 중인 메인 스레드가 깨어나 다음 테스트를 진행합니다.
      • 장점: 정확한 이벤트 발생 횟수를 기다릴 수 있어 예측 불가능한 sleep 시간을 방지하며, 테스트 실패 시 실제 코드의 문제점을 명확히 파악할 수 있게 합니다.
  • 적용 방법: 메타프로그래밍을 사용하여 테스트 대상 메서드(check_condition)를 재정의하고, 원래 로직 실행 후 `CountdownLatch

countdown`을 호출하도록 주입합니다. 이 방법을 통해 원본 소스 코드를 변경하지 않고도 스레드 동작을 안정적으로 테스트할 수 있습니다.

결론

결론적으로, 발표자는 '분할 정복' 기법을 통해 핵심 로직을 스레드로부터 분리하고, `concurrent-ruby` Gem의 `CountdownLatch`를 활용하여 스레드 간의 동기화를 정밀하게 제어함으로써 멀티스레드 Ruby 코드의 테스트를 안정화하고 효율성을 높일 수 있음을 강조합니다. 이러한 접근 방식은 개발자가 예측 불가능한 대기 시간으로 인한 테스트 실패에 좌절하는 대신, 테스트 실패가 실제 코드의 문제점을 명확히 나타내도록 하여 개발 시간을 절약하고 코드 품질을 향상시키는 데 기여합니다. 궁극적으로 개발자가 자신의 테스트를 통제하고 원하는 대로 동작하게 만들 수 있도록 합니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

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