멀티스레드 코드의 불안정한 테스트를 개선하기 위해 두 가지 주요 전략이 제시됩니다. 이는 개발자가 테스트를 더 잘 제어하고 예측 불가능한 요소를 줄이는 데 중점을 둡니다.
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를 위한 동시성 툴킷으로,CountdownLatch는ConditionVariable을 기반으로 하지만 훨씬 간결하고 사용하기 쉽습니다.- 작동 방식: 특정 카운트(기대하는 이벤트 발생 횟수)를 설정하고, 스레드 내에서 각 이벤트가 발생할 때마다
countdown메서드를 호출하여 카운트를 감소시킵니다. 카운트가 0이 되면 대기 중인 메인 스레드가 깨어나 다음 테스트를 진행합니다. - 장점: 정확한 이벤트 발생 횟수를 기다릴 수 있어 예측 불가능한
sleep시간을 방지하며, 테스트 실패 시 실제 코드의 문제점을 명확히 파악할 수 있게 합니다.
- 작동 방식: 특정 카운트(기대하는 이벤트 발생 횟수)를 설정하고, 스레드 내에서 각 이벤트가 발생할 때마다
- Ruby Condition Variables: Ruby는
- 적용 방법: 메타프로그래밍을 사용하여 테스트 대상 메서드(
check_condition)를 재정의하고, 원래 로직 실행 후 `CountdownLatch