
Scala 프로젝트의 CI 실행 시간이 약 27분에서 평균 4분으로 줄었다. 테스트를 줄이거나 데이터베이스를 모의(mock)하지 않고, 불필요한 작업을 건너뛰고 테스트 환경과 캐시를 손본 결과다. 제시된 절감 시간은 측정 범위가 서로 달라 단순히 합산할 수 없다.
대상은 오픈소스 가계부 프로젝트다. 코드 약 3만 2천 줄과 테스트 1만 5천 줄로 구성된 풀스택 Scala 앱이며, 약 1천 개 테스트 중 600개는 PostgreSQL이 필요하고 그중 300개는 앱도 실행해야 한다. 테스트 대부분은 모의 객체 대신 실제 PostgreSQL 컨테이너와 앱 엔드포인트를 사용한다. 기존 CI는 코드 형식, 앱 컴파일, 테스트, 프런트엔드 실행, 프로덕션 빌드를 확인했다.
목표는 테스트 범위를 유지하고 새 인프라나 비용을 들이지 않으면서 실행 시간을 줄이는 것이었다. Bazel로 옮기거나 Play Framework를 교체하는 등 큰 코드 변경도 피했다. 각 개선안은 가설로 세우고 실제 CI에서 따로 측정한 뒤 효과가 있는 것만 적용했다.
불필요한 작업과 반복 실행 줄이기
프런트엔드 변경에는 백엔드 테스트가, 백엔드 변경에는 프런트엔드 테스트가 필요하지 않도록 PR이 바꾼 파일에 따라 검사 대상을 나눴다. 일반적인 PR에서는 작업량이 줄고, 최악의 경우에는 전체 검사를 실행한다. 테스트가 통과했을 때는 테스트 결과 보고서를 만들지 않아 작업마다 약 30초를 아꼈다.
테스트에서는 실제 bcrypt를 유지하되 비용 설정을 가장 낮게 해 약 25초를 절약했다. 데이터베이스가 필요한 테스트마다 PostgreSQL 컨테이너를 띄우던 방식은 전체 테스트 실행에서 컨테이너 하나를 공유하도록 바꿔 약 15분을 줄였다. 이에 맞춰 테스트 데이터를 무작위로 만들고 관련 데이터만 조회하도록 고쳤다. SELECT COUNT(*) FROM table처럼 테이블 전체를 세는 쿼리는 더는 사용하지 않는다.
실제 앱이 필요한 테스트는 15개 스위트로 묶여 있다. 테스트마다 앱을 띄우는 대신 스위트마다 한 번 실행해 약 70초를 줄였다. 모든 스위트가 앱 하나를 공유하면 스위트 병렬 실행과 충돌할 수 있고, 추가 절감 폭도 작아 스위트 단위 실행을 택했다.
빌드 도구 sbt는 워크플로마다 최소 6번 실행됐고, 한 번 시작하는 데 최대 30초가 걸렸다. 서버 모드를 사용해 한 번 띄운 빌드 서버를 각 단계에서 재사용하자 약 1분을 절약했다.
병렬 실행과 캐시 점검
CI 실행기의 여러 코어를 활용해 테스트를 병렬 실행하면서 약 40초를 더 줄였다. 2코어 가상 머신에서는 효과가 거의 없어 보였지만, 4코어 CI 실행기에서는 시간이 단축됐다. 병렬 실행 때 빌드가 멈추던 코드 오류를 먼저 찾아 고쳤으며, 사례는 실제 실행 환경에서 성능을 측정해야 한다는 점을 보여준다.
기존에 캐시를 설정해 두었지만, 캐시 키가 적절하지 않았고 저장 단계도 실패하고 있었다. GitHub Actions에서는 PR 브랜치에서 저장한 캐시를 해당 PR에서만 사용할 수 있고, main이나 다른 PR에서는 볼 수 없다. 캐시 접근 제한 문서에도 이 제약이 설명돼 있다. 캐시 저장 단계가 계속 느린 점을 확인하고 접힌 ‘Post’ 작업을 펼쳐 본 뒤 저장 실패를 발견했다. 캐시가 보존되지 않아 실행마다 건너뛰려던 작업을 다시 수행하고 있었다.
캐시를 세 종류로 나눴다. 컴파일 산출물은 최신 main에서 복원하고 저장은 main에서만 하도록 해 PR의 업로드를 생략했다. 의존성 캐시와 Scala용 ScalablyTyped 생성 코드 캐시도 별도로 구성했다.
- 컴파일 산출물: 약 2분 절약. PR은 복원만 하고 업로드하지 않아 약 30초를 추가로 절약한다.
- 의존성: 약 30초 절약.
- 생성 코드: 약 20초 절약.
다음 단계와 측정 결과
남은 개선 방향은 여러 머신과 PR 사이에서 컴파일 결과를 공유하는 원격 빌드 캐시다. 빌드 도구의 다음 주요 버전인 sbt 2.0은 표준 캐시 백엔드를 지원하지만, 이를 쓰려면 캐시 백엔드가 필요하다. Bazel로 전면 이전하면 원격 캐시 효과를 얻을 수 있지만 비용이 더 커 제외했다. 워크플로를 여러 병렬 작업으로 나누는 방법도 검토했지만, 각 작업이 새로 시작해 체크아웃·의존성 설치·컴파일을 반복하므로 4분 실행에서는 CI 사용량에 비해 시간 절감이 작다고 판단했다.
아래 PR별 시간은 테스트 단계, 첫 sbt 단계, 전체 PR 빌드 등 서로 다른 범위에서 측정했다. 따라서 서로 더해 전체 절감 시간을 계산할 수 없다. #22와 #25의 절감량은 단일 전후 비교로 분리해 측정한 값이 아니라 추정치다.
| PR | 변경 | 절감 시간 | 측정 범위 |
|---|---|---|---|
| #22 | 테스트 전체에서 PostgreSQL 컨테이너 하나 공유 | 약 15분(추정) | 테스트마다 컨테이너 실행에서 공유 방식으로 변경. #27과 함께 sbt test가 약 20분에서 5분 미만으로 줄었다. |
| #25 | 단계마다 재시작하는 대신 실행 중인 빌드 서버 재사용(sbt thin client) | 약 1분(추정) | sbt 실행 약 6회에서 2회로 감소해 반복적인 초기 실행을 피했다. |
| #27 | 테스트마다 실행하던 Play 앱을 스위트마다 실행 | 약 70초 | 서버 테스트 단계 183초에서 113초로 감소. |
| #28 | 변경된 경로에 맞춰 테스트와 빌드 범위 제한 | 약 20~40초 | PR이 웹 또는 서버 한쪽만 바꾼 경우. |
| #30 | 확인된 의존성 캐시 | 약 28초 | 첫 sbt 단계 78초에서 50초로 감소. |
| #31 | ScalablyTyped 프런트엔드 타입 정의 캐시 | 약 21초 | 첫 sbt 단계 50초에서 29초로 감소. |
| #33 | 실행은 됐지만 저장되지 않던 캐시 단계 교체 | 약 29초 | 낭비되던 ‘Post Setup Scala’ 단계 제거. |
| #37 | CI 코어 수에 맞춰 테스트 병렬 실행 | 약 43초 | 서버 테스트 단계 122초에서 79초로 감소. |
| #38 | 테스트에서 비용 설정을 가장 낮춘 실제 bcrypt 사용 | 약 25초 | HTTP 수준 테스트 59초에서 34초로 감소. |
| #39 | 빌드 실패 때만 테스트 결과 보고서 생성 | 약 30초 | 성공한 실행마다 절약. |
| #40 | main에서 갱신한 컴파일 결과 캐시를 PR에서 복원만 하도록 변경 | 약 100초 | 캐시가 준비된 PR 빌드 4분 32초에서 2분 52초로 감소. |
전체 CI 시간은 약 27분에서 약 4분으로 줄어 약 85% 단축됐다. 테스트와 앱은 그대로 유지했다. 같은 변경은 Scala 웹앱 템플릿에도 적용됐다.
부록: 원문 예시
Warning: Failed to save: <h2>Our services aren't available right now</h2><p>We're working to restore all services as soon as possible. Please check back soon.</p>...
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요