Backend Development

CI가 5분씩 걸려서 뜯어봤더니... 린트(Lint)에 무거운 의존성을 태우고 있었다

Kun Woo Kim 2026. 1. 23. 16:05
728x90
반응형

Lint에 PyTorch를 설치하고 있었다.


발단

PR 올릴 때마다 CI가 5분씩 걸렸다. 코드 한 줄 고쳐서 올렸는데 초록불 보려면 5분. 커피 타 오기엔 애매하고, 가만히 기다리기엔 긴 시간이다.

"원래 CI가 이렇게 오래 걸리나?"

아니었다. 뜯어보니 Lint job에서 PyTorch, transformers, ultralytics를 설치하고 있었다. ruff 돌리려고.


문제의 워크플로우

# Before: ci.yml
jobs:
  lint:
    name: Lint & Type Check
    steps:
      - name: Install Poetry
        uses: snok/install-poetry@v1
      - name: Install dependencies
        run: poetry install --no-interaction --no-root  # 여기서 PyTorch 설치
      - name: Run Ruff
        run: poetry run ruff check app/

  test:
    name: Test
    needs: lint  # Lint 끝나야 Test 시작

이 구조의 문제점:

총 시간: 3분 51초 (직렬 실행)
  1. Lint에 전체 의존성 설치 - ruff 돌리는 데 PyTorch가 필요 없다
  2. 순차 실행 - Lint 끝날 때까지 Test가 대기
  3. 시간 낭비 - 둘이 독립적인데 왜 기다리나?

수정 1: Lint에서 불필요한 의존성 제거

ruff는 정적 분석 도구다. 코드를 실행하지 않는다. 그런데 왜 poetry install로 런타임 의존성을 전부 설치하고 있었을까?

그냥 관성이었다. "Lint job이니까 Poetry 셋업하고 의존성 설치하고..." 복붙의 폐해다.

# After: Lint job
- name: Install ruff
  run: pip install ruff  # 3초

- name: Run Ruff
  run: ruff check app/

결과: 1분 41초 → 10초


수정 2: Lint와 Test 병렬 실행

# Before
test:
  needs: lint  # Lint 완료 후 Test 시작

# After
test:
  # needs 제거 → Lint와 동시 시작

build:
  needs: [lint, test]  # 둘 다 성공해야 Build

needs를 제거하면 GitHub Actions가 두 job을 병렬로 실행한다.

Before:
[Lint] ──────→ [Test]
 1m 41s         2m 10s
              총 3분 51초

After:
[Lint]  ────────┐
  10s           ├──→ [Build]
[Test]  ────────┘       42s
 2m 21s
              총 3분 3초 (Test 기준)

Lint가 10초면 끝나니까, 사실상 Test 시간 + Build 시간이 전체 CI 시간이 된다.


최종 구조

name: CI

on: push

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install ruff
        run: pip install ruff
      - name: Run Ruff
        run: ruff check app/

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install Poetry
        uses: snok/install-poetry@v1
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
      - name: Install dependencies
        run: poetry install --no-interaction --no-root
      - name: Run tests
        run: poetry run pytest

  build:
    name: Build Docker Image
    needs: [lint, test]  # 둘 다 성공해야 실행
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: docker build -t app .

결과 비교

항목 Before After
Lint 의존성 Poetry 전체 설치 pip install ruff
Lint 시간 1m 41s 10s
실행 방식 순차 (Lint → Test) 병렬 (Lint ∥ Test)
전체 CI ~4분+ ~3분

체감상 40초 정도 단축. 숫자로 보면 대단해 보이진 않지만, PR 10번 올리면 7분 절약이다.


추가로 할 수 있는 것들

1. Docker layer caching

현재는 docker build만 하고 있다. docker/build-push-actioncache-from/cache-to 옵션을 쓰면 레이어 캐싱이 가능하다.

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

2. poetry.lock 커밋 확인

key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

이 캐시 키가 제대로 작동하려면 poetry.lock 파일이 레포에 커밋되어 있어야 한다. 없으면 매번 캐시 미스.

3. Test 분할 실행

테스트가 많아지면 pytest-split으로 병렬화할 수 있다.

test:
  strategy:
    matrix:
      group: [1, 2, 3]
  steps:
    - run: poetry run pytest --splits 3 --group ${{ matrix.group }}

교훈

CI 최적화의 핵심은 "이 job에 이게 진짜 필요한가?"를 묻는 것이다.

Lint에 PyTorch가 필요한가? 아니다.
Lint가 끝나야 Test를 시작할 수 있나? 아니다.

당연하다고 생각했던 것들을 의심하면 의외로 쉽게 시간을 줄일 수 있다.

728x90
반응형