1. 테스트 성능의 최대 적: 데이터베이스 쓰기
테스트 스위트가 느려지는 가장 흔한 원인은 과도한 데이터베이스 쓰기 작업입니다. Rails 생태계에서 표준처럼 사용되는 FactoryBot은 강력하지만, 오용될 경우 성능에 치명적일 수 있습니다.
- 최소한의 베이스 팩토리: 베이스 팩토리는 객체가 유효하게 저장되기 위한 최소한의 필드만을 포함해야 합니다. 불필요한 연관 관계(Association)를 베이스 팩토리에 포함하면, 단일 객체 생성 시 수십 개의 연관 객체가 함께 생성되는 연쇄 반응을 일으켜 성능을 저하시킵니다.
- 전략적 객체 생성: 데이터베이스 저장이 반드시 필요하지 않은 경우
build나build_stubbed를 사용하거나, 단순히User.new를 사용하여 메모리 내에서만 객체를 다루는 것이 좋습니다. - 로컬리티(Locality)의 중요성: 테스트에 필요한 특정 데이터만 테스트 코드 내에서 명시적으로 설정함으로써, 테스트의 의도를 명확히 하고 불필요한 데이터 생성을 방지할 수 있습니다.
2. Fixtures와 FactoryBot의 철학적 대립
테스트 데이터를 관리하는 두 도구는 성능과 유지보수 측면에서 서로 다른 장단점을 가집니다.
- Fixtures (성능의 승자): Fixtures는 테스트 실행 전 데이터를 한 번에 데이터베이스에 로드하므로 속도가 매우 빠릅니다. 하지만 테스트 코드 외부에 데이터가 존재하여 ‘미스터리 게스트’ 문제를 일으킬 수 있으며, 팀원 간에 공유되는 ‘데이터 시나리오(Shared Story)’를 유지해야 하는 관리 부담이 있습니다.
- FactoryBot (유연성의 승자): 각 테스트마다 필요한 데이터를 생성하므로 독립성이 높고 이해하기 쉽습니다. 하지만 매번 발생하는 데이터베이스 I/O 비용으로 인해 Fixtures보다 느릴 수밖에 없습니다.
- 팀 규모에 따른 선택: 소규모의 안정적인 팀은 Fixtures의 공유 컨텍스트를 효율적으로 관리할 수 있지만, 인원 변동이 잦거나 대규모 팀의 경우 FactoryBot의 명시적인 데이터 생성이 협업에 더 유리할 수 있습니다.
3. 테스트 피라미드와 시스템 테스트의 관리
모든 테스트가 동일한 가치를 가지는 것은 아니며, 비용 대비 효율을 고려한 배치가 필요합니다.
- 시스템 테스트의 비용: 브라우저를 구동하는 시스템 테스트는 가장 느리고 비용이 많이 듭니다. 따라서 피라미드 상단에 최소한으로 유지해야 합니다.
- 테스트 삭제의 미학: 버그 수정을 위해 작성한 복잡한 시스템 테스트가 역할을 다했다면, 이를 유지하는 대신 더 가벼운 단위 테스트(Unit Test)로 대체하고 시스템 테스트는 삭제하는 용기가 필요합니다. 모든 에지 케이스를 시스템 테스트로 검증하는 것은 CI 시간을 무한정 늘리는 결과를 초래합니다.
- Testing Trophy: JavaScript 커뮤니티에서 제안된 이 개념은 단위 테스트와 엔드투엔드(E2E) 테스트 사이의 중간 단계 테스트에 집중하여 실질적인 안정성을 확보하는 대안적 모델을 제시합니다.
4. 병렬화(Parallelization)의 수학과 한계
병렬화는 테스트 시간을 단축하는 가장 직접적인 방법이지만, 단순한 워커 증설만으로는 해결되지 않는 지점이 존재합니다.
- 부하 불균형(Load Balancing): 테스트 파일마다 실행 시간이 다르기 때문에, 단순히 파일 개수로 나누면 특정 워커만 오래 실행되는 ‘텐트폴(Tentpole)’ 현상이 발생합니다. 이를 해결하기 위해 실행 기록(Timing file)을 기반으로 부하를 분산하거나, 중앙 큐(Queue) 시스템을 사용하는
Knapsack Pro같은 도구가 권장됩니다. - 부팅 오버헤드(Boot Time): 각 워커는 Rails 애플리케이션을 부팅하는 비용을 지불해야 합니다. 부팅에 1분이 걸리는 앱에서 테스트 실행 시간이 1분 미만으로 떨어지면, 워커를 추가해도 부팅 비용 때문에 전체 소요 시간은 거의 줄어들지 않는 한계 비용 체감의 법칙이 적용됩니다.
- 최적화의 세 레버: 병렬화 수준, 전체 테스트 실행 로직의 효율성, 그리고 애플리케이션 부팅 속도라는 세 가지 요소를 동시에 고려하여 최적의 지점을 찾아야 합니다.