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초 (직렬 실행)- Lint에 전체 의존성 설치 - ruff 돌리는 데 PyTorch가 필요 없다
- 순차 실행 - Lint 끝날 때까지 Test가 대기
- 시간 낭비 - 둘이 독립적인데 왜 기다리나?
수정 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-action의 cache-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를 시작할 수 있나? 아니다.
당연하다고 생각했던 것들을 의심하면 의외로 쉽게 시간을 줄일 수 있다.
'Backend Development' 카테고리의 다른 글
| Vercel KV로 배너 클릭 추적 시스템 만들기: Redis를 서버리스에서 사용하는 법 (0) | 2026.01.01 |
|---|---|
| 파이썬 GIL과 멀티스레딩: 언제 멀티스레드가 효과적일까? (7) | 2025.07.06 |
| CI/CD 파이프라인: 개발부터 배포까지 자동화의 모든 것 (5) | 2025.07.04 |
| 의존성 주입(Dependency Injection): 유연하고 테스트 가능한 코드 만들기 (0) | 2025.07.02 |
| 네트워크 IP 할당의 모든 것: 정적 IP vs 동적 IP, 무엇을 선택해야 할까? (1) | 2025.06.27 |
