IT/CI CD

Reusable Workflow vs Composite Action, 1년 같이 굴려본 결론

gfrog 2026. 5. 19. 21:13
반응형

작년 봄에 우리 팀 CI/CD 표준을 정리하면서 GitHub Actions의 Reusable Workflow와 Composite Action을 둘 다 도입했다. 그땐 "쓰임새가 다르니까 둘 다 쓰자"가 결론이었는데, 1년 굴려보니 둘의 경계가 처음 생각보다 훨씬 명확하게 갈렸다. 그리고 최근 GitHub이 2026 Actions 보안 로드맵을 발표하면서 Composite action의 nested dependency 노출 방식, scoped secrets 같은 변경이 들어오기 시작해서, 이참에 한번 정리해두면 좋을 것 같다.

이 글은 단순히 둘의 문법 차이를 비교하는 글은 아니다. 실제로 우리 팀이 어떤 기준으로 갈라 썼고, 어디서 후회했고, 지금 어떻게 운영하는지에 대한 회고에 가깝다.

단도직입적으로, 우리는 8:2로 갈랐다

Reusable workflow가 8, Composite action이 2 정도다. 처음에는 거의 반반이었는데 점점 한쪽으로 쏠렸다. 이유는 단순하다. Composite action으로 시작했다가 결국 Reusable workflow로 옮긴 케이스는 많은데, 반대 케이스는 거의 없었다.

Composite으로 시작했다가 Reusable로 옮긴 전형적인 경우는 이렇다.

처음에는 actions/setup-node + cache + lint + test 정도를 묶어서 frontend-ci-steps라는 Composite action으로 만들었다. 한동안 잘 돌아갔다. 그러다 어느 순간 PR 단위로 lint와 test를 병렬화하고 싶어졌다. Matrix로 Node 버전 3개 돌리고 싶기도 했다. Composite action 안에서는 job이 하나뿐이라 병렬화가 안 됐다. 결국 Reusable workflow로 옮겼다.

반대로 Reusable workflow로 시작했다가 "이건 너무 무겁네" 하면서 Composite으로 내린 경우는 거의 없었다. Reusable은 잡 단위라 살짝 무겁다는 느낌은 있어도, 무겁다고 망하는 일은 없으니까.

그럼 Composite action은 언제 쓰나

우리 팀 기준으로 정리하면, Step 두세 개를 묶어서 한 줄로 호출하고 싶을 때다. 그 이상은 다 Reusable로 간다.

대표적으로 이런 것들:

  • aws-oidc-login — OIDC 토큰으로 IAM role assume, 캐싱까지 묶음
  • setup-toolchain — asdf 기반 언어 런타임 일괄 설치
  • docker-buildx-cache-warmup — Buildx 셋업 + 캐시 워밍

전부 "단일 job 안에서 step 몇 개를 합친다" 수준이다. 잡 자체를 만들거나 다른 잡과 의존관계를 가질 일이 없다.

여기서 작년에 한번 데인 적이 있다. Composite action 안에서 id: deploy-result 같은 출력을 잡 레벨로 노출하고 싶었는데, Composite action의 output을 잡 output으로 그대로 끌어내려고 했더니 outputs: result: ${{ steps.xxx.outputs.yyy }} 식으로 한 단계 더 매핑해야 했다. 처음 보면 좀 헷갈린다. 이 시점에서 "아, 이건 Reusable로 가야겠다" 하는 신호다.

시크릿: 작년에 가장 많이 발 묶인 부분

작년 5월쯤 우리 팀에서 가장 많이 시간을 까먹은 게 시크릿이었다. 정확히는 Reusable workflow의 시크릿 전달 방식이다.

Reusable workflow는 호출하는 쪽 시크릿을 자동으로 상속받지 않는다. 명시적으로 넘겨줘야 한다.

# 호출하는 워크플로우
jobs:
  deploy:
    uses: org/.github/.github/workflows/deploy.yml@v3
    secrets:
      AWS_DEPLOY_ROLE: ${{ secrets.AWS_DEPLOY_ROLE }}
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

secrets: inherit로 통째로 넘기는 옵션도 있긴 한데, 우리 팀은 작년 하반기부터 inherit을 금지했다. 이유는 "넘기지 말아야 할 시크릿이 호출 컨텍스트에 같이 있는 케이스"를 막기 위해서다. 호출하는 워크플로우가 prod 시크릿까지 들고 있으면, inherit 하나로 staging 배포 reusable이 prod 시크릿도 받아버리는 경로가 열린다.

GitHub이 2026 보안 로드맵에서 발표한 scoped secrets도 이 흐름이다. 시크릿이 실행 컨텍스트에 명시적으로 바인딩되는 방향으로 가고 있다. 우리가 inherit 금지로 운영하던 게 결과적으로 옳은 방향이었다는 게 좀 위안이긴 했다.

Composite action은 시크릿 개념 자체가 없다. 입력으로 받아야 하는데, 받는 순간 그 값은 step level에서 그대로 expose된다. ${{ inputs.token }} 같은 식. 그래서 우리는 시크릿이 끼는 작업은 Composite로 안 만든다. 시크릿이 들어가야 한다면 Reusable이거나, OIDC로 in-job에서 직접 federate 한다.

버전 핀: SHA냐 태그냐

여기는 의외로 의견이 갈렸다. 작년 초까지는 우리 팀도 @main을 쓰고 있었다. 진짜 부끄러운 얘기인데, 어떤 PR 머지 한 번이 모든 repo의 CI에 즉시 반영되는 구조였다. 한번 setup-node 호출부에서 인풋 이름 하나 바꿨다가 30개 넘는 repo의 PR이 동시에 빨개졌다. 그 뒤로 다 SHA 핀으로 옮겼다.

지금 운영 규칙은 이렇다:

  • prod 워크플로우 → SHA 핀 필수
  • staging/dev 워크플로우 → 태그 핀 허용 (@v3)
  • 새 액션 검증용 sandbox repo → @main 허용

SHA 핀이 가독성은 떨어진다. uses: org/repo/.github/workflows/build.yml@a1b2c3d4... 보다는 @v3이 사람 눈에는 좋다. 그래서 Renovate에 GitHub Actions digest pinning을 켜놨다. PR이 자동으로 올라오고 changelog까지 코멘트에 박혀서 들어온다. 이건 작년에 도입한 것 중에 만족도 상위권에 든다.

결론, 굳이 정리하자면

지금 누가 "둘 중에 뭘 쓸까요" 물어보면 나는 이렇게 답한다.

잡 하나 안에서 step 묶는 거면 Composite. 잡 자체를 재사용하거나 matrix 돌리거나 시크릿이 끼면 Reusable. 애매하면 Reusable.

마지막 줄이 핵심이다. Reusable로 시작해서 무거우면 Composite로 내리는 건 거의 안 일어난다. 반대 방향 마이그레이션은 자주 일어난다. 그러면 처음부터 Reusable로 시작하는 게 평균적으로 손해가 적다.

물론 우리 팀 워크로드 기준이다. 모노레포 한 곳에서 모든 잡이 닫혀 도는 팀이라면 Composite 비중이 더 클 수도 있다. 다른 팀에선 어떻게 갈라 쓰는지 궁금하긴 하다.

반응형