Reddit

From 27 minutes to 4: Cutting CI build time by 85%

CI 빌드 시간을 27분에서 4분으로, 85% 단축하기

Scala 풀스택 프로젝트의 CI가 27분 걸리던 원인을 실제 CI에서 하나씩 측정해 평균 4분으로 줄였습니다. 테스트별 Postgres 컨테이너와 앱 구동을 공유하고, 변경 경로에 따른 작업 생략과 캐시 수정, 병렬 실행을 적용했습니다.

에디터 노트

가장 큰 절감은 테스트를 덜 실행한 데서가 아니라 테스트마다 반복하던 환경 준비를 공유한 데서 나왔습니다. Postgres 컨테이너 공유만 약 15분을 줄였고 테스트 데이터 격리 방식도 함께 손봤습니다. 변경 경로에 따라 프론트엔드와 백엔드를 나누는 방식은 이 모노레포에서 양쪽 변경 시 전체를 실행한다는 조건이 있습니다. 댓글의 반론처럼 여러 시스템에 걸친 E2E 흐름을 파일 경로만 보고 안전하게 생략하기는 어렵습니다. 병렬 실행 결과는 러너의 코어 수에 따라 달라졌습니다. 저자는 2코어 개발 환경에서는 느려지고 4코어 CI에서는 빨라졌다고 밝혔으며, 다른 댓글도 코어를 모두 쓰면 오히려 느려질 수 있다고 경험을 나눴습니다. 따라서 85%라는 전체 결과는 이 프로젝트의 테스트 구성과 CI 환경에서 측정한 값으로 읽는 편이 정확합니다. 캐시 저장 실패를 로그에서 찾아낸 사례와 항목별 절감 수치가 서로 다른 측정 범위라는 설명은 재현할 때 유용한 근거입니다.

AI 요약

CI를 27분에서 평균 4분으로 줄인 과정을 실제 측정 결과와 함께 정리합니다. 대상은 Scala로 만든 3만 2천 줄 규모의 풀스택 앱이며 테스트 코드는 1만 5천 줄입니다. 약 1천 개 테스트 가운데 600개가 Postgres를 사용하고, 그중 300개는 앱 인스턴스까지 띄웁니다. 기존 구성은 테스트마다 깨끗한 Postgres 컨테이너와 앱을 새로 시작해 격리성은 높았지만 테스트가 늘수록 시작 비용도 누적됐습니다. 저자는 Bazel 이전이나 테스트 축소 없이 Pull Request CI 시간을 줄이고, 새 인프라 비용도 들이지 않는다는 조건을 세웠습니다.

작업량 줄이기

프론트엔드와 백엔드 중 변경된 쪽만 빌드하고 테스트하도록 파일 경로를 분류했습니다. 양쪽을 함께 바꾸면 전체 작업을 실행합니다. 초록색 빌드에서는 테스트 결과 보고서를 만들지 않아 매번 약 30초를 아꼈습니다. 테스트에서 실제 bcrypt를 유지하되 비용 설정을 가장 낮춰 약 25초를 줄였습니다.

테스트 환경 재사용

테스트마다 만들던 Postgres 컨테이너를 전체 테스트 실행에서 하나만 공유해 약 15분을 절약했습니다. 공유 과정에서 테스트 데이터가 서로 섞이지 않도록 무작위 데이터를 쓰고, 전체 행 수를 세는 대신 필요한 데이터만 조회하도록 테스트를 고쳤습니다. 앱 테스트 약 300개는 15개 스위트로 나뉘어 있습니다. 테스트마다 앱을 띄우던 방식에서 스위트마다 한 번 띄우는 방식으로 바꿔 해당 단계 시간을 183초에서 113초로 줄였습니다.

빌드 도구 sbt도 워크플로에서 최소 여섯 번 시작했고, 시작에 최대 30초가 걸렸습니다. sbt 서버를 한 번 띄운 뒤 각 단계가 클라이언트로 연결하도록 바꿔 약 1분을 절약했습니다. 테스트 병렬 실행으로도 약 43초를 줄였습니다. 개발용 2코어 VM에서는 병렬 실행이 오히려 1.5배 오래 걸렸지만, 4코어 GitHub Actions 러너에서는 테스트 단계가 122초에서 79초로 짧아졌습니다. 병렬 실행으로 빌드가 멈추던 코드 버그도 먼저 수정했습니다.

캐시와 측정

기존 캐시 설정은 저장 키가 적절하지 않았고, 캐시 저장 액션도 매번 실패했습니다. 저장 단계가 계속 느린 점을 확인하고 접힌 로그를 펼친 뒤 실패 메시지를 발견했습니다. GitHub Actions에서는 PR 브랜치에 저장한 캐시를 메인 브랜치나 다른 PR에서 사용할 수 없다는 점도 고려해 구성을 다시 만들었습니다. 컴파일 결과물은 메인 브랜치에서만 저장하고 PR은 복원만 하도록 했습니다. 의존성과 생성 코드도 별도 캐시로 나눴습니다. 의존성 캐시로 약 28초, 생성된 프론트엔드 타입 정의 캐시로 약 21초를 줄였고, 컴파일 결과물 캐시는 따뜻한 PR 빌드 시간을 4분 32초에서 2분 52초로 단축했습니다.

각 PR의 시간 절감 수치는 테스트 단계, sbt 첫 실행, 전체 빌드 등 서로 다른 범위에서 측정했으므로 더해 전체 절감 시간으로 볼 수 없습니다. Postgres 공유와 sbt 서버는 각각 단일 전후 비교가 아닌 추정치입니다. 전체 27분에서 약 4분으로 줄었다는 결과는 이 조건을 구분해 제시합니다. 다음 단계로 원격 빌드 캐시를 언급하지만, 이를 쓰려면 캐시 백엔드가 필요합니다. 여러 병렬 작업으로 워크플로를 쪼개는 방식은 매 작업이 환경과 의존성을 다시 준비하므로 현재 4분 수준에서는 선택하지 않았습니다.

Reddit 반응

  • @pineapplepizzabong — 저는 사용 가능한 코어의 50%만 쓰거나 적어도 100%까지 쓰지 않을 때 대체로 성능이 더 좋았습니다. 테스트를 돌려 보니 CI 머신을 완전히 포화시키는 것보다 여유를 남기는 편이 항상 나았습니다.
  • @editor_of_the_beast — 대기행렬 이론에서 잘 알려진 특성입니다. 자원 사용률이 100%에 가까워지면 서비스 시간이 초선형으로 증가합니다.
  • @AlexITC — GitHub Actions의 경우 VM에는 코어가 4개 있습니다. 앱 테스트 단계는 병렬 실행을 하지 않았을 때보다 두 배 더 걸렸습니다. 제가 개발하는 2코어 VM에서는 병렬 실행 시 테스트 시간이 1.5배로 늘었습니다.
  • @ctheune — 저는 Postgres, Redis, Chrome을 쓰는 테스트 10개를 12코어에서 돌립니다. RAM이 병목일 때도 있습니다. 10~20GB 정도 필요합니다.
  • @AlexITC — 코어가 정말 많네요. GitHub Actions 작업자에는 코어 4개와 메모리 16GB가 있고, Postgres가 필요한 테스트 약 600개와 LocalStack 같은 S3가 필요한 테스트도 몇 개 돌립니다. Chrome은 확실히 무겁습니다.
  • @Pokeputin — 계약 업무를 홍보하는 글이라는 점은 이해하지만, 결국 누구나 쓰고 문서에도 나오는 아주 기본적인 최적화 선택지를 적용한 내용입니다.
  • @AlexITC — 홍보 글은 아닙니다. 일거리를 얻을 것 같지는 않지만 연락처를 적어 두는 게 나쁠 건 없었습니다. 최적화가 복잡하거나 영리하다고 말한 적도 없습니다. 간단한 변경으로 실행 시간이 크게 줄 수 있다는 점을 보여주려 했습니다. 캐시를 처리해 줄 거라고 생각한 GitHub Actions가 매번 실패했는데 접힌 로그 아래 숨겨져 있었습니다. 기본 브랜치와 PR 사이의 캐시 제한도 몰랐습니다.
  • @kerakk19 — 첫 번째 항목은 쓸모가 없습니다. 세 번째 항목인 캐시가 처리해야 합니다. 나머지는 괜찮습니다.
  • @AlexITC — 도구에 따라 다릅니다. Bazel은 테스트 결과를 캐시하고 Gradle은 변경이 없으면 테스트 작업을 최신 상태로 보고 생략할 수 있습니다. 하지만 sbt 1.x, Maven, Jest, RSpec은 기본 설정에서 전부 다시 실행합니다. 캐시가 있어도 CI 실행 사이에 공유하지 않으면 매번 새 러너에서 다시 돌게 됩니다. 여러 시스템을 가로지르는 복잡한 E2E 흐름에서 바뀐 부분만 골라 테스트하기 어렵다는 지적에는 동의합니다. 여기서는 세밀한 테스트 선별이 아니라 모노레포의 백엔드만 바뀌면 프론트엔드 테스트를 건너뛰는 방식이며, 양쪽을 바꾸면 전체를 실행합니다.
  • @cykio — 빌드와 테스트 시간을 줄이려고 하는 중인데, AI는 빌드와 테스트 로그만 줘도 낭비되는 작업을 찾는 데 꽤 능숙합니다.
  • @AlexITC — 글의 bcrypt 최적화는 AI가 제안했습니다. 다른 제안 중에는 효과가 없다고 하거나 엉뚱한 방향을 가리킨 것도 있었습니다.
  • @double-you — 서버를 시작하고 다시 시작하는 시간은 큰 낭비입니다. 그 부분부터 살펴본 건 좋습니다. 테스트 병렬 실행도 시간을 줄이며 부하 테스트에도 유용할 수 있습니다.

원문: alexitc.com / 번역·요약: Trawling