EN
HomePost

테스트 만들기는 쉬워졌는데, 이 많은 테스트를 어떻게 다 돌리지?

2026-08-01

AI와 함께 테스트가 빠르게 늘어난 뒤 CI에서 나타난 두 종류의 flaky test를 MongoDB 인덱스 초기화와 CPU throttling 관점에서 추적하고 해결한 경험

AI와 함께 코딩하면서 테스트를 만드는 비용이 0에 가까워지고 있다.

예전에는 기능을 하나 만든 뒤 테스트 케이스를 직접 나열하고, fixture를 준비하고, 비슷한 코드를 반복해서 작성해야 했다. 지금은 구현과 동시에 테스트 초안을 만들 수 있다. 빠진 경계 조건을 찾아달라고 할 수도 있다. 그러다 보니 테스트의 양이 체감상 기하급수적으로 늘었다.

문제는 테스트 생성 비용만 줄었다는 것이다. 만들어진 테스트를 실행하는 CPU 비용, 데이터베이스를 준비하는 비용, 실패를 분석하는 비용은 사라지지 않았다. 오히려 매 CI 실행마다 계속 누적됐다.

우리 팀에서는 이 간극이 flaky test로 나타났다. 같은 timeout처럼 보였지만 실제 원인은 두 가지였다.

  1. MongoDB 테스트가 인덱스를 반복해서 생성하고 있었다.
  2. CI 작업과 테스트 worker가 한꺼번에 실행되며 CPU throttling이 발생하고 있었다.

둘은 증상만 같았을 뿐, 전혀 다른 문제였다.

테스트가 많아지자 CI가 흔들리기 시작했다

우리는 TypeScript와 MongoDB 기반의 웹·모바일 애플리케이션을 모노레포로 운영한다. CI는 GitHub Actions를 사용하고, Kubernetes 위에 self-hosted runner를 띄운다. 인프라 비용을 줄이기 위해 runner가 실행되는 노드에는 spot instance도 활용하고 있다.

처음에는 별문제가 없었다. 테스트 수가 적을 때는 CI도 충분히 빨랐고, 간헐적인 실패도 거의 없었다.

하지만 AI와 함께 테스트를 추가하는 속도가 빨라지면서 상황이 달라졌다. 테스트 실행시간이 계속 길어졌고, 어느 순간부터 MongoDB를 사용하는 테스트에서 알 수 없는 timeout이 나타나기 시작했다.

항상 실패하는 것도 아니었다. 같은 코드가 어떤 때는 통과하고, 어떤 때는 timeout으로 실패했다. 다시 실행하면 통과하기도 했다. 전형적인 flaky test였다.

첫 번째 원인: 테스트마다 반복되던 인덱스 생성

MongoDB 테스트에서는 Mongo Memory Server를 사용하고 있었다. 실제 MongoDB와 가까운 환경에서 쿼리와 저장 로직을 확인할 수 있다는 장점이 있지만, 테스트가 늘수록 초기화 비용도 함께 커졌다.

처음에는 Mongo Memory Server를 사용하는 테스트 자체를 없앨까 고민했다. mock으로 바꾸면 빠르고 단순해질 것 같았다. CI를 안정시키기 위해 통합 테스트의 신뢰도를 포기해야 하나 싶었다.

그런데 시간을 잡아먹던 부분을 좁혀보니, 문제는 메모리 DB 자체가 아니었다. 인덱스 생성이 테스트 과정에서 반복되고 있었다.

테스트가 적을 때는 이 비용이 눈에 띄지 않았다. 테스트 묶음이 많아지고 여러 worker에서 동시에 실행되자 인덱스를 준비하는 시간이 누적됐다. 부하가 낮을 때는 제한시간 안에 끝났지만, 조금만 느려지면 timeout을 넘었다.

해결 방법은 MongoDB 테스트를 없애는 것이 아니라 실행 구조를 바꾸는 것이었다.

Mongo Memory Server를 사용하는 테스트를 별도의 Jest 실행 묶음으로 분리했다. 그리고 비싼 인덱스 설정을 개별 테스트 도중에 반복하지 않고, 해당 테스트 묶음이 시작할 때 한 번에 처리했다. 초기 준비 단계에는 필요한 만큼 시간을 주되, 테스트가 실행되는 중간에는 같은 인덱스를 계속 만들지 않도록 했다.

비용이 사라진 것은 아니다. 여러 번 나눠서 지불하던 비용을 처음에 한 번만 지불하도록 옮긴 것이다.

이 변경으로 MongoDB 테스트의 timeout은 해결됐다. 여기서 끝난 줄 알았다.

그런데 DB와 상관없는 테스트도 실패했다

얼마 뒤 다른 timeout이 보이기 시작했다. 이번에는 MongoDB를 사용하지 않는 테스트였다. 평소에는 충분히 빠르게 끝나는 테스트가 CI에서 가끔 제한시간을 넘겼다.

처음에는 앞서 겪은 문제의 연장이라고 생각하기 쉽다. 에러 메시지는 똑같이 timeout이고, 다시 실행하면 통과하는 현상도 같았다.

하지만 DB와 관계없는 테스트까지 동시에 느려진다는 점이 결정적인 차이였다. 테스트 코드나 MongoDB 초기화가 아니라, 테스트가 실행되는 환경을 보기 시작했다.

결정적인 단서는 CPU throttling이었다.

두 번째 원인: 두 겹의 병렬 실행

Kubernetes 호스트 하나에 CI 작업이 몰리는 시점에는 CPU throttling이 발생하고 있었다. Kubernetes에서는 컨테이너가 설정된 CPU limit에 가까워지면 커널이 CPU 사용을 제한한다. Kubernetes 공식 문서도 CPU limit이 throttling 방식으로 강제된다고 설명한다.

중요한 점은 테스트 병렬화가 한 군데에만 있지 않았다는 것이다.

text
CI에서 동시에 실행하는 작업 수 × 각 테스트 러너의 worker 수
= 실제 동시 실행 규모

우리 환경에서는 GitHub Actions가 여러 테스트 작업을 병렬로 실행하고, 각 작업 안에서는 Jest가 다시 여러 worker를 만들었다. 두 설정을 따로 보면 과해 보이지 않았지만, 곱한 결과는 달랐다. 최악의 경우 약 120개의 테스트 실행이 한꺼번에 CPU를 두고 경쟁할 수 있었다.

병렬화를 많이 하면 무조건 빨라질 것 같지만, 가용 CPU를 넘어서면 반대가 된다. 각 작업이 CPU를 조금씩 나눠 쓰고, throttling과 스케줄링 지연이 늘어난다. 평소 10초 안에 끝나던 테스트가 가끔 10초를 넘는 이유도 여기에 있었다.

CI와 테스트 러너 양쪽에 상한을 뒀다

우리는 두 단계의 병렬도를 함께 줄였다.

GitHub Actions의 matrix job은 max-parallel을 3으로 제한했다. 이 설정은 동시에 실행할 matrix job 수를 제한한다. 자세한 동작은 GitHub Actions 공식 문서에서 확인할 수 있다.

yaml
strategy:
  max-parallel: 3
  matrix:
    # 테스트 작업들

각 작업 안에서 실행되는 Jest는 maxWorkers를 4로 제한했다. Jest에서 maxWorkers는 테스트 실행을 위해 생성하는 worker pool의 상한이다. Jest 공식 문서에서도 CI처럼 자원이 제한된 환경에서는 이 값을 조정할 수 있다고 안내한다.

typescript
export default {
  maxWorkers: 4,
  testTimeout: 10_000,
};

결과적으로 동시에 실행될 수 있는 규모를 약 120개에서 12개 수준으로 줄였다.

text
GitHub Actions 3개 × Jest worker 4개 = 최대 12개

여기서 34가 모든 CI의 정답이라는 뜻은 아니다. 중요한 것은 CI의 병렬도와 테스트 러너의 병렬도를 따로 보지 않는 것이다. GitLab CI와 Vitest를 사용하든, Jenkins와 다른 JavaScript 테스트 러너를 사용하든 계산 방식은 같다.

timeout을 늘리는 대신 다시 10초로 통일했다

문제를 해결하기 전에는 timeout 설정도 점점 흐트러지고 있었다. 실패하는 테스트가 생길 때마다 어떤 테스트는 10초, 어떤 테스트는 30초로 제한시간을 늘렸다.

당장은 CI가 통과하지만 원인은 그대로 남는다. 다음에 호스트 부하가 더 높아지면 30초도 넘을 수 있다. timeout이 커질수록 진짜로 멈춘 테스트를 발견하는 시간도 늦어진다.

MongoDB 인덱스 초기화와 인프라 병렬도를 각각 수정한 뒤에는 기본 timeout을 다시 10초로 통일했다. 오래 걸릴 수밖에 없는 초기 준비 단계와 개별 테스트의 제한시간도 분리해서 생각할 수 있게 됐다.

그리고 간헐적으로 발생하던 flaky test가 사라졌다.

같은 timeout을 어떻게 구분할까

돌아보면 두 문제는 같은 에러 메시지 뒤에 숨어 있었지만, 단서가 달랐다.

구분반복되는 DB 초기화인프라 CPU throttling
주로 실패한 범위MongoDB를 사용하는 테스트DB와 무관한 테스트까지 불규칙하게 실패
부하의 위치인덱스와 스키마 준비 과정CI 작업과 테스트 worker 전체
결정적인 단서인덱스 생성 구간에 실행시간 집중호스트 부하와 CPU throttling 증가
해결DB 테스트 분리, 인덱스 설정을 초반에 한 번만 실행CI 병렬도와 worker 수를 함께 제한

비슷한 문제를 만난다면 다음 순서로 확인하는 것이 좋다.

  1. 실패가 특정 DB 테스트에만 몰리는지 확인한다.
  2. 테스트마다 setup, migration, schema 또는 index 생성이 반복되는지 확인한다.
  3. DB와 무관한 테스트도 같은 시간대에 느려지는지 확인한다.
  4. CI 작업 수와 테스트 worker 수를 곱해 실제 동시 실행 규모를 계산한다.
  5. 실패 시점의 CPU throttling을 확인한다.
  6. timeout을 늘리기 전에 초기화 구조와 병렬도를 먼저 조정한다.

테스트를 만드는 것만큼 돌리는 구조도 중요해졌다

AI는 테스트를 만드는 비용을 크게 낮췄다. 테스트가 많아지는 것 자체는 좋은 일이다. 이전에는 비용 때문에 생략했을 경계 조건까지 확인할 수 있고, 변경에 대한 안전망도 더 촘촘해진다.

대신 병목이 다른 곳으로 이동했다. 이제는 테스트 코드를 작성하는 시간보다, 수많은 테스트를 어떻게 격리하고 어떤 순서와 병렬도로 실행할지 설계하는 일이 더 중요해지고 있다.

우리 사례에서 timeout은 하나였지만 원인은 둘이었다. 하나는 테스트 구조 안에 있었고, 다른 하나는 테스트를 돌리는 인프라에 있었다. 테스트 수가 빠르게 늘어나는 시대에는 둘 다 테스트 아키텍처의 일부로 봐야 한다.