Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby on Rails 병렬 테스트 완벽 가이드: 잠재적 위험과 해결 전략

테스트가 너무 빠르다고 불평하는 개발자를 본 적이 있습니까? 저는 없습니다.

빠른 테스트는 곧 빠른 피드백을 의미합니다. 로컬 환경에서든 CI(지속적 통합) 파이프라인에서든, 테스트가 일찍 끝날수록 실패에 더 빨리 대응하고 코드를 개선할 수 있습니다. 생산성 향상 외에도, 느린 테스트가 개발자를 짜증나게 만든다는 사실은 잘 알려져 있습니다. 짜증난 개발자를 좋아하는 사람은 없죠.

그렇다 하더라도 번개처럼 빠른 테스트 스위트를 만드는 일은 생각만큼 쉽지 않습니다. 다행히 Rails 6에는 병렬 테스트(parallel testing)라는 강력한 기능이 도입되었습니다. 시작하기 매우 간단하면서도 테스트 속도를 크게 높일 수 있지만, 주의해야 할 함정들도 존재합니다.

병렬 테스트란 무엇인가?

병렬 테스트는 정확히 무엇을 의미할까요?

테스트 스위트를 실행하면 테스트 러너는 보통 하나의 프로세스를 생성해 테스트를 하나씩 차례로 실행합니다. 즉 직렬(serial) 방식입니다. 단일 테스트 프로세스는 CPU 코어 하나만 사용합니다. 짐작하시겠지만, 이 방식은 수십 개의 CPU 코어를 갖춘 현대적인 하드웨어를 제대로 활용하지 못합니다.

10개의 CPU 코어를 가진 멋진 맥북을 사용하고 있어도, 안타깝게도 테스트 속도는 빨라지지 않습니다!

이 문제는 개별 테스트를 여러 워커 프로세스에 분배함으로써 해결할 수 있습니다. 그러면 테스트가 차례로 실행되는 것이 아니라 동시에 실행됩니다. 즉 병렬(parallel)로 실행되는 것입니다. 두 개의 워커로 테스트 스위트를 실행하면 단일 워커보다 두 배 빠릅니다.

머신의 코어가 많을수록 더 많은 워커 프로세스를 사용할 수 있고, 따라서 테스트 스위트도 더 빨리 끝납니다. 예를 들어 테스트 실행에 보통 8분이 걸린다면, 4개의 워커 프로세스를 사용하면 약 2분으로 단축됩니다!

16코어 머신에서 16개의 워커를 띄운다면 어떨까요? 정말 훌륭하겠죠!

Rails에서 병렬 테스트 설정하기

그럼 어떻게 시작할까요? 예전에는 서드파티 젬(gem)을 사용해야 했지만, Rails 6부터는 병렬 테스트가 기본 내장되어 있습니다. 테스트에 parallelize를 추가하기만 하면 됩니다:


이 설정을 사용하면 Rails는 머신의 프로세서 수에 따라 자동으로 워커 프로세스를 생성합니다. 또한 테스트 실행을 위해 네임스페이스가 지정된 데이터베이스(예: database-test-0, database-test-1 등)를 생성합니다.

시작하기 위해 필요한 것은 이게 전부입니다! 물론 필요하다면 추가 설정 옵션도 있습니다.

때로는 병렬 테스트를 위한 특정 설정이나 정리 작업이 필요할 수 있습니다. Rails는 이를 위한 두 가지 훅을 제공합니다 — parallelize_setupparallelize_teardown입니다. 새 워커 프로세스가 생성되기 전후에 호출됩니다:


워커 수를 직접 지정할 수도 있습니다:


또는 PARALLEL_WORKERS 환경 변수를 사용해 기존 설정을 재정의할 수 있습니다:


워커 대신 스레드를 사용해 테스트 스위트를 병렬화하는 옵션도 있습니다.


with: :threads는 JRuby나 TruffleRuby를 사용할 때의 기본 옵션입니다. 이론적으로 스레드는 프로세스보다 오버헤드가 적기 때문에 약간 더 나은 성능을 제공합니다. 하지만 실제로는 스레드 방식이 크게 유용하다고 느낀 적이 없으며, 대부분의 경우 프로세스 기반 병렬화를 고수하는 편이 안전합니다.

함정 조심하기

그렇다면 기존 테스트에 parallelize만 추가하면 놀라운 속도 향상을 경험할 수 있을까요? 정말 그렇게 쉬울까요?

운이 좋다면 실제로 그럴 수 있습니다. 하지만 기존 테스트 스위트에 병렬화를 처음 적용할 때 예상치 못한 문제에 부딪힐 가능성이 더 높습니다. 저 역시 그랬습니다!

먼저 한 가지 짚고 넘어갈 것이 있습니다. Minitest가 아닌 RSpec을 사용한다면 운이 좋지 않습니다. RSpec은 Rails 6의 내장 병렬 테스트를 지원하지 않습니다. 이를 변경하려는 논의가 진행 중이지만 한동안 큰 진전이 없었습니다. RSpec으로 병렬 테스트를 구현하려면 grosser/parallel_tests 같은 서드파티 젬을 사용하는 것이 최선입니다.

또 다른 예상치 못한 문제는 소수의 테스트를 병렬로 실행하면 직렬로 실행할 때보다 오히려 느려질 수 있다는 점입니다. 병렬 테스트 설정에는 여러 데이터베이스 생성 등 상당한 오버헤드가 따르며, 이는 병렬화로 얻을 수 있는 이점을 상쇄할 수 있습니다.

소수의 테스트에 대해서는 병렬 테스트를 비활성화하는 편이 나을 수 있습니다. PARALLEL_WORKERS 환경 변수를 사용하면 됩니다:


Rails 7에서는 많은 수의 테스트를 실행할 때만 병렬 실행을 활성화하여 이 문제를 해결했습니다. 이미 업그레이드했다면 이 문제를 겪지 않을 것입니다. 기본 병렬화 임계값은 50으로 설정되어 있지만 재정의할 수 있습니다:


마지막으로 강조하고 싶은 문제는 가장 자주 마주치면서도 가장 교활하고 다루기 어려운 문제입니다. 병렬 테스트를 활성화하면 테스트가 무작위로 실패하기 시작할 수 있습니다. 병렬화가 어떻게 이런 문제를 일으키는지 이해하기 위해 간단한 테스트 케이스를 살펴보겠습니다:


약간 무의미하기는 하지만 이 테스트는 완전히 정상입니다. 직렬로 실행하는 한 100% 통과합니다. 각 테스트는 서로 독립적이며, 무작위 순서로 실행해도 실패하지 않습니다. 그러나 여러 프로세스나 스레드가 섞이면 상황이 달라집니다.

병렬로 테스트를 실행하면 CPU 스케줄링 때문에 테스트 내 개별 문장의 실행 순서가 바뀔 수 있습니다. 예제에서는 때때로 다음과 같은 실행 순서를 볼 수 있습니다:


워커 2가 워커 1의 assertion이 실행되기 전에 파일을 삭제했기 때문에 첫 번째 테스트는 — 때때로 — 실패합니다. 설상가상으로 실행 순서에 따라 두 번째 테스트가 실패하고 첫 번째 테스트가 통과하는 경우도 발생합니다.

이 간단한 예제는 파일뿐 아니라 테스트가 스레드 세이프하지 않은 방식으로 접근하는 모든 싱글턴 리소스로 확장되는 문제를 보여줍니다. Redis 데이터베이스나 Elasticsearch 인덱스에 쓰기를 한다면 비슷한 문제를 겪을 가능성이 높습니다. 더 나쁜 것은, 무작위 실패를 일으키는 모든 테스트를 찾아내는 데 시간이 걸리고, 그것들을 모두 수정하는 데는 더 많은 시간이 든다는 점입니다.

불안정한(flaky) 병렬 테스트를 해결할 만한 만능 해결책은 없습니다. 일반적으로 여러 테스트 프로세스가 리소스를 공유하지 않도록 보장해야 합니다. 파일에는 Tempfile을 사용하고, parallelize_setup을 활용해 네임스페이스가 지정된 리소스(예: Redis 데이터베이스)를 생성하는 식으로 말입니다.

기존 Rails 테스트에 병렬 테스트 추가하기

병렬화로 인한 무작위 테스트 실패로 고생 중이고 이를 고칠 시간이 없다고 가정해 봅시다. 그래도 병렬 테스트의 이점은 누리고 싶을 것입니다. 이 경우 테스트의 일부에만 병렬 테스트를 활성화하는 편이 좋습니다.

parallelize를 호출하는 테스트만 병렬화되므로, concern이나 부모 클래스를 활용하면 테스트 클래스를 하나씩 병렬 테스트에 추가할 수 있습니다. 다음과 같은 모듈을 만들 수 있습니다:


이 모듈을 포함하는 테스트 클래스는 이제 병렬로 실행됩니다:


또는 ParallelTest 같은 새로운 테스트 클래스를 만들 수도 있습니다:


그런 다음 병렬로 실행해야 하는 테스트는 이 클래스를 상속받고, 문제가 되는 테스트는 제외하면 됩니다.

임시방편으로서의 병렬 테스트

병렬 테스트는 적은 노력으로 인상적인 속도 향상을 제공합니다. 하지만 속지 마세요. 병렬 테스트는 테스트 스위트 속도를 개선하는 다른 접근법을 대체하는 것이 아니라 보완재입니다.

테스트 스위트가 느리다는 것을 발견했고 시간을 낼 수 있다면, 프로파일링을 통해 느려지는 근본 원인을 파악하고 해결하는 데 시간을 투자하세요. 병렬 테스트를 추가한 느린 테스트 스위트는 빨라지겠지만, 이미 빠른 테스트 스위트를 병렬로 실행하는 것만큼 빠르지는 않습니다!

마무리

이 글에서는 병렬 테스트가 무엇인지, 어떻게 설정하고 구성하는지 살펴보았습니다. 테스트 속도를 높일 방법이 필요하다면 병렬 테스트가 딱 그 역할을 해줍니다.

테스트 스위트에 병렬 테스트를 추가할 때 몇 가지 장애물에 부딪힐 수 있습니다. 수년간 안정적으로 실행되던 테스트가 갑자기 실패하기 시작해도 놀라지 마세요. 테스트 스위트의 일부만 병렬화하는 방식으로 충분히 우회할 수 있습니다.

어떤 접근법을 선택하든, 병렬 테스트는 테스트 속도를 높이는 훌륭한 도구입니다!

즐거운 코딩 되세요!

P.S. Ruby Magic 포스트가 발행되는 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 포스트도 놓치지 마세요!

Ruby on Rails 병렬 테스트 완벽 가이드: 잠재적 위험과 해결 전략

Hans-Jörg Schnedlitz

게스트 저자 Hans는 오스트리아 빈 출신의 Rails 엔지니어입니다. 대부분의 시간을 코딩하거나 코딩 관련 자료를 읽으며 보내고, 가끔은 자신의 블로그에 글을 쓰기도 합니다! 화면 앞에 앉아 있지 않을 때는 아마 산을 오르고 있을 겁니다.

Hans-Jörg Schnedlitz의 모든 글 보기