← 개발 로그 목록

website: CI 파이프라인 FE/BE 잡 분리 및 백엔드 병렬도 버그 수정

/ 9분 분량 / 개발 로그

프론트엔드와 백엔드 코드가 한 잡(job)에서 순차적으로 검증되던 CI 파이프라인을, 변경된 경로에 따라 필요한 잡만 병렬로 돌도록 분리해서 빌드 시간을 줄였습니다. 동시에 백엔드 쪽에서 발생하던 병렬도 관련 버그도 함께 손봤습니다.

요약

2026년 7월 21일에 머지된 PR #137 "ci: 백엔드 병렬도 버그 수정 + FE/BE 잡 분리로 CI 단축"의 내용을 정리합니다. 기존에는 build-and-test라는 단일 잡 안에서 백엔드 빌드/테스트를 돌렸는데, 이번 작업으로 changes → backend/frontend → comment 구조로 잡을 나눴습니다. 수정된 파일은 .github/workflows/ci.yml과 backend/build.gradle 두 개이고, 전체적으로 75줄이 추가되고 8줄이 삭제됐습니다.

배경 및 목적

기존 CI는 프론트엔드 변경이든 백엔드 변경이든 상관없이 하나의 잡에서 처리하고 있었던 것으로 보입니다. 문제는 두 가지였습니다.

  1. 프론트엔드만 고친 PR에서도 불필요하게 백엔드 관련 작업(혹은 그 반대)이 함께 실행돼 CI 시간이 늘어남
  2. 백엔드 테스트 실행 시 병렬도 설정에 버그가 있어서 정확한 검증이 안 되고 있었음

두 문제를 한 번에 해결하기 위해 커밋 메시지 그대로 "백엔드 병렬도 버그 수정"과 "FE/BE 잡 분리로 CI 단축"을 같이 작업했습니다.

구현 내용

잡 분리 구조

가장 큰 변화는 .github/workflows/ci.yml에 dorny/paths-filter@v3를 이용한 changes 잡을 추가한 것입니다.

changes:
  runs-on: ubuntu-latest
  outputs:
    backend: ${{ steps.filter.outputs.backend }}
    frontend: ${{ steps.filter.outputs.frontend }}
  steps:
    - name: 변경 경로 감지
      uses: dorny/paths-filter@v3
      id: filter
      with:
        filters: |
          backend:
            - 'backend/**'
            - 'shared/**'
          frontend:
            - 'frontend/**'
            - 'shared/**'

이 잡의 결과(output)를 받아서 backend, frontend 잡이 각각 if: needs.changes.outputs.backend == 'true' 조건으로 실행 여부를 결정합니다. shared/** 경로는 양쪽 필터에 다 걸리게 해서, 공용 코드가 바뀌면 두 잡이 모두 도는 구조로 만들었습니다.

기존에 하나였던 build-and-test 잡은 그대로 backend 잡으로 이름만 바뀌었고, 여기에 새로 frontend 잡을 추가했습니다.

frontend:
  needs: changes
  if: needs.changes.outputs.frontend == 'true'
  runs-on: ubuntu-latest
  steps:
    - name: 소스코드 체크아웃
      uses: actions/checkout@v4
    - name: Node 20 세팅
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: npm
        cache-dependency-path: frontend/package-lock.json
    - name: 의존성 설치
      working-directory: frontend
      run: npm ci
    - name: 린트
      working-directory: frontend
      run: npm run lint
    - name: 빌드 (타입체크 포함)
      working-directory: frontend
      run: npm run build

프론트엔드는 Node 20 세팅 후 npm ci로 의존성을 설치하고, 린트와 빌드(타입체크 포함)를 순서대로 돌리도록 했습니다. cache: npm으로 의존성 캐시를 설정해서 매번 새로 설치하지 않도록 한 부분도 눈에 띕니다.

PR 코멘트 로직 변경

원래는 잡이 하나였기 때문에 job.status만 보고 성공/실패 코멘트를 달면 됐는데, 잡이 두 개로 나뉘면서 이 로직도 손봐야 했습니다.

const backendRan = '${{ needs.changes.outputs.backend }}' === 'true';
const frontendRan = '${{ needs.changes.outputs.frontend }}' === 'true';
const backendResult = '${{ needs.backend.result }}';
const frontendResult = '${{ needs.frontend.result }}';

const failed = (backendRan && backendResult !== 'success') || (frontendRan && frontendResult !== 'success');

실행되지 않은 잡(스킵된 잡)은 실패로 취급하지 않도록 backendRan, frontendRan 플래그로 먼저 걸러주고, 실행된 잡 중에 실패한 게 있는지만 판단하게 만들었습니다. 코멘트 본문에도 백엔드 ✅ · 프론트엔드 ✅ 같은 식으로 각 잡의 결과를 요약해서 보여주도록 바꿨습니다.

const parts = [];
if (backendRan) parts.push(`백엔드 ${backendResult === 'success' ? '✅' : '❌'}`);
if (frontendRan) parts.push(`프론트엔드 ${frontendResult === 'success' ? '✅' : '❌'}`);
const summary = parts.join(' · ');

comment 잡의 needs도 [changes, backend, frontend]로 바뀌었고, if: always() 조건은 유지해서 앞선 잡들이 실패하거나 스킵되더라도 코멘트는 항상 남도록 했습니다.

백엔드 병렬도 버그 수정

backend/build.gradle도 함께 수정됐습니다. 상세 diff가 이번 데이터에는 포함되지 않았지만, 커밋 메시지의 "백엔드 병렬도 버그 수정"이라는 표현으로 미루어보면 테스트 실행 시 병렬 처리(maxParallelForks 등) 설정에 문제가 있어서 이를 함께 고친 것으로 보입니다. FE/BE 잡을 나누는 김에 백엔드 잡 자체의 실행 안정성도 같이 점검한 셈입니다.

기술적 의사결정

paths-filter를 선택한 이유

경로 기반으로 잡 실행 여부를 나누는 방법은 여러 가지가 있습니다. GitHub Actions 자체의 paths/paths-ignore 트리거 필터를 워크플로우 레벨에서 쓸 수도 있었지만, 이 경우 PR 코멘트처럼 항상 실행돼야 하는 잡과 조건부로 실행돼야 하는 잡을 같은 워크플로우 안에서 섞어 쓰기가 까다롭습니다. dorny/paths-filter는 하나의 워크플로우 안에서 output을 통해 이후 잡들의 조건을 세밀하게 제어할 수 있다는 점에서 이번 구조에 더 잘 맞았습니다.

  • 장점: 워크플로우 트리거는 그대로 두고, 잡 단위로 유연하게 조건을 걸 수 있음. shared/** 같은 공용 경로도 필터 조합으로 쉽게 처리 가능
  • 단점: changes 잡이 먼저 체크아웃하고 diff를 계산해야 하므로, 아주 작은 오버헤드가 추가됨. 다만 이 정도는 FE/BE 각각 불필요한 빌드를 건너뛰는 이득에 비하면 감수할 만한 수준

배운 점 및 개선점

잡을 나누고 나니 단순히 "빨라졌다"로 끝나는 게 아니라, PR 코멘트 로직처럼 부수적으로 손봐야 하는 부분이 꽤 있다는 걸 다시 느꼈습니다. 잡이 하나일 때는 job.status 하나만 보면 됐는데, 잡을 나누는 순간 "실행되지 않은 잡을 실패로 볼 것인가"라는 문제가 새로 생기고, 이걸 놓치면 프론트엔드만 바꾼 PR에서도 백엔드가 실패한 것처럼 잘못된 코멘트가 달릴 수 있었습니다. 이번에 backendRan/frontendRan 플래그로 이 부분을 명확히 구분한 게 핵심이었습니다.

앞으로는 shared/** 필터에 걸리는 파일이 늘어날 경우 두 잡이 자주 같이 도는 상황이 생길 수 있어서, 실제로 CI 시간이 얼마나 단축됐는지 몇 주 지켜보면서 필터 기준을 다시 조정할 필요가 있어 보입니다. 백엔드 병렬도 버그도 이번엔 build.gradle 설정만 고쳤는데, 관련 테스트 케이스가 병렬 실행 환경에서도 안정적으로 통과하는지 별도로 검증하는 절차를 다음에 추가하면 좋을 것 같습니다.

참고 자료