이 프로젝트는 테스트 스위트 최적화를 위해 여러 단계에 걸친 심층적인 분석과 개선 작업을 포함했습니다.
1. 프로파일링을 통한 숨겨진 로거 발견
- 초기 진단:
TEST_STACK_PROF=1 SAMPLE=200 bundle exec rspec spec
명령과speedscope.app
을 사용하여 200개 테스트 샘플을 프로파일링했습니다. - 병목 현상 식별: StackProf 보고서에서 과도한 데이터베이스 활동(Trilogy#query), 외부 서비스 호출(Stripe, OpenAI), 그리고 상당한 로깅 활동이 주요 용의자로 지목되었습니다.
- 숨겨진 로거 해결:
- Rails의 상세 SQL 로깅:
config/environments/test.rb
에서config.active_record.verbose_query_logs = false
,config.active_record.query_log_tags_enabled = false
,config.log_level = :fatal
설정을 통해 비활성화했습니다. 이는 전체 프로젝트에서 가장 큰 성능 향상을 가져왔습니다. - Sentry 커스텀 로거:
Sentry::Testing.fake!
를config.before(:suite)
에 추가하여 테스트 환경에서 Sentry 로깅을 비활성화했습니다.
- Rails의 상세 SQL 로깅:
- 결과: 이 변경 사항들로 단일 프로세스 실행 시간은 25분에서 12분으로, CI 실행 시간(16개 병렬 프로세스)은 4분에서 2분으로 각각 절반으로 단축되었습니다.
2. 팩토리 오버헤드 관리
- 다음 병목 현상: 로깅 문제가 해결된 후, 프로파일러는 객체 생성, 특히 FactoryBot 사용으로 인한 오버헤드를 지목했습니다.
- TagProf 활용:
TAG_PROF_FORMAT=html TAG_PROF=type TAG_PROF_EVENT=sql.active_record,factory.create,sidekiq.inline bundle exec rspec
명령으로factory.create
이벤트가 총 실행 시간의 절반 이상을 차지함을 확인했습니다. - 전략 1:
let_it_be
: TestProf의let_it_be
를 사용하여describe
블록 내 모든 예제에서 동일한 데이터를 한 번만 생성하고 재사용하여 불필요한 데이터베이스 레코드 생성을 줄였습니다. 단, 상태 누수에 주의해야 합니다. - 전략 2:
FactoryDefault
: 중복 연관 객체 생성을 줄이기 위해FPROF=1 FACTORY_DEFAULT_PROF=1 bundle exec rspec spec/services/some_heavy_spec.rb
로 프로파일링하여 불필요하게 생성되는 연관 객체를 식별했습니다.create_default
를 사용하여 공통 연관 객체를 한 번만 생성하고 재사용하도록 변경했습니다.
3. 병렬화 전환 및 테스트 격리 문제 노출
parallel_tests
의 한계: 기존parallel_tests
gem은 테스트 파일을 프로세스에 미리 할당하여, 느린 테스트가 있는 워커는 다른 워커가 유휴 상태일 때까지 전체 빌드 시간을 지연시키는 문제가 있었습니다.test-queue
로 전환: 개별 테스트 예제를 중앙 큐에 넣고 워커가 완료 즉시 다음 작업을 가져가는test-queue
로 전환하여 모든 워커가 끝까지 바쁘게 작동하도록 했습니다.- 예상치 못한 결과: 전환 후 100개 이상의 테스트가 실패하는 “A sea of red” 상황이 발생했습니다. 이는
test-queue
의 높은 동시성 환경이 그동안 숨겨져 있던 테스트 간의 의존성과 상태 누수를 여과 없이 드러낸 결과였습니다.
4. 불안정한 테스트 격리 해결
- 사례 1: 시간 여행 및 역설:
- 전역
Timecop.freeze
누수:Timecop.freeze
후Timecop.return
이 호출되지 않아 다음 테스트에 시간이 고정되는 문제가 발생했습니다.rails_helper.rb
에config.after(:each) { Timecop.return }
을 추가하여 해결했습니다. - Doorkeeper 미스터리: 병렬 워커에서
Timecop.freeze
로 인해 과거 시간으로 고정된 상태에서 다른 테스트가 현재 시간에 유효한 Doorkeeper 토큰을 생성하면, 검증 시 만료된 것으로 처리되어 403 오류가 발생했습니다.rspec --bisect
로 원인을 찾아 불필요한Timecop.freeze
호출을 제거하여 해결했습니다.
- 전역
- 사례 2: 전역 오염 정리:
- 상수 충돌: 동일한 이름의 상수가 다른 값으로 정의되어 마지막에 실행된 상수가 적용되는 문제가 발생했습니다. 상수의 이름을 파일별로 고유하게 변경하여 해결했습니다.
- 캐시 및 설정 누수:
PermissionsManager
가 역할을 전역적으로 메모리에 캐싱하여 테스트 간에 오래된 데이터가 유출되었습니다.rails_helper.rb
의before
훅에서PermissionsManager::SystemRoles.clear_cache!
를 호출하여 해결했습니다. 전역 gRPC 클라이언트가 재설정되지 않는 문제도after
훅을 통해nil
로 재설정하여 해결했습니다. - Sidekiq Enterprise
unique!
모드 누수: 특정 테스트에서 Sidekiq Enterprise의unique!
모드를 검증해야 했으나, 이 전역 설정이 다른 테스트에 영향을 주어 실패를 유발했습니다.around
훅을 사용하여unique!
모드를 활성화하고 테스트 실행 후ensure
블록에서Sidekiq::Enterprise::Unique::Client
미들웨어를 제거하여 완벽한 격리를 보장했습니다.