GitHub Actions Reusable Workflow vs Composite Action, 뭘 쓸까

한 6개월 전쯤 팀 내 GitHub Actions 파이프라인을 정리하는 작업을 했다. 서비스가 40개쯤 되니까 각 리포마다 조금씩 다른 CI 파이프라인이 굴러가고 있었고, "이거 좀 통일하자"는 얘기가 나온 지가 오래였다. 그때 처음 진지하게 부딪힌 질문이 "Reusable workflow로 뺄까, Composite action으로 뺄까"였다.
결론부터 말하면 지금은 상황에 따라 둘 다 쓴다. 근데 처음엔 그냥 "재사용"이라는 단어만 보고 Reusable workflow만 팠다가 몇 번 삽질했다. 이 글은 그 삽질을 기반으로 두 방식을 비교하고, 우리 팀이 현재 쓰는 기준을 정리한 거다.
근본적인 차이: 잡(job)이냐 스텝(step)이냐
가장 큰 차이는 재사용의 단위다. Reusable workflow는 잡 전체를 재사용한다. uses: org/repo/.github/workflows/build.yml@sha 같은 식으로 잡을 통째로 부르는 거고, 그래서 자기만의 러너, 자기만의 permissions, 자기만의 시크릿을 갖는다. 반면 Composite action은 여러 스텝을 묶은 하나의 스텝이다. 이미 돌고 있는 잡 안에서 uses:로 불려서, 그 잡의 컨텍스트 안에서 실행된다.
이 차이가 실무에서 뭘 만드느냐 하면 이런 거다. 예를 들어 "Docker 이미지 빌드 → SBOM 생성 → 취약점 스캔 → ECR 푸시" 이 전체 파이프라인을 여러 서비스에 공유하고 싶다? 이건 Reusable workflow다. 반면에 "AWS OIDC 로그인 후 특정 IAM 롤 assume"만 여러 워크플로우에서 재사용하고 싶다? 이건 Composite action.
또 다른 실용적 차이는 secrets 전달 방식이다. Reusable workflow는 secrets: inherit나 명시적으로 시크릿을 넘겨줘야 하지만, Composite action은 호출한 잡의 환경을 그대로 물려받는다. 이게 편할 때도 있고 위험할 때도 있는데, 편의성 측면에선 Composite이 낫고 격리 측면에선 Reusable이 낫다.
실무에서 자주 겪는 함정
Reusable workflow의 첫 번째 함정은 needs로 순서 잡기가 애매하다는 것이다. Reusable workflow를 부르면 그 자체가 하나의 잡(들)이 되니까, 호출한 워크플로우의 다른 잡과 의존 관계를 만들려면 잡 단위로 붙여야 한다. 조합이 늘어나면 워크플로우 파일의 구조가 이상해진다. 처음에 이걸 몰라서 Reusable workflow 안에서 여러 잡을 chain으로 묶고, 바깥에서 또 그걸 부르는 식으로 짰다가 디버깅에 3시간 날렸다.
Composite action은 반대 함정이 있다. 로그 그룹핑이 어중간하다. Composite 내부에서 여러 스텝을 실행하면 로그에 그 하위 스텝들이 뭉쳐서 표시된다. 개별 스텝의 실행 시간이나 실패 지점을 파악하기가 Reusable workflow보다 좀 더 귀찮다. 특히 팀원 중에 GitHub Actions에 익숙하지 않은 사람이 있으면 "왜 이 로그가 여기 뭉쳐있어?" 하는 질문이 종종 나온다.
또 하나. Composite action은 매트릭스나 병렬 실행을 그 안에서 못 만든다. Composite은 그냥 스텝일 뿐이니까 자기 자식 스텝을 병렬로 돌릴 방법이 없다. 반면 Reusable workflow는 그 자체가 잡이라 매트릭스를 자유롭게 쓴다. 그래서 "빌드를 여러 아키텍처(arm64, amd64)에 대해 병렬로 돌리자" 같은 요구가 있으면 자연스럽게 Reusable workflow로 간다.
버전 관리도 조금 다르다. 둘 다 SHA 핀 또는 태그로 참조하는 게 원칙인데, 최근에도 GitHub 커뮤니티 가이드에서 프로덕션에서는 @main 쓰지 말고 SHA 핀하라는 얘기가 반복해서 나오고 있다. 다만 Reusable workflow는 리포 전체를 참조하는 거라 리포 자체의 브랜치 보호 규칙까지 신경 써야 하고, Composite action은 태그를 좀 더 가볍게 돌릴 수 있다.
우리 팀은 이렇게 나눈다
기준을 만들어놓고 나서는 크게 헷갈리진 않는다. 지금 우리 팀의 규칙은 대략 이렇다.
Reusable workflow로 뺀다:
- 서비스 전체가 공유하는 표준 파이프라인 (build → test → scan → deploy)
- OIDC 기반 클라우드 인증이 필요하고 permissions를 별도로 관리해야 하는 경우
- 매트릭스 병렬 실행이 필요한 경우
- 완전히 독립된 러너/시크릿 컨텍스트가 필요한 경우 (특히 프로덕션 배포 잡)
Composite action으로 뺀다:
- "특정 도구 설치 + 캐싱 + 환경변수 세팅" 같은 스텝 조합
- 한 잡 안에서 여러 번 반복되는 스텝 패턴
- 다른 워크플로우들의 각기 다른 잡 안에서 부분적으로만 쓰이는 로직
- 대략 3~7개 스텝을 묶는 규모
숫자로 감을 잡자면, 지금 우리 조직에는 Reusable workflow 12개, Composite action 30개쯤 있다. Composite이 훨씬 많다. Composite은 작은 단위라 가볍게 뽑아내기 좋고, Reusable은 잡 전체를 표준화할 때만 쓴다.
한 가지 더. 최근에 정리하면서 concurrency 그룹 설정과 dependency 캐싱을 재사용 로직에 통합했는데, 이것만 해도 우리 CI 러닝 타임이 눈에 띄게 줄었다. 커뮤니티 가이드에서도 concurrency + 캐싱 조합이 아키텍처 변경 없이 실행 시간 40~60% 정도 단축 효과가 있다고 얘기하는데, 우리 케이스에서도 대략 그 정도 나왔다. 재사용 자체보다도 재사용하는 김에 이런 걸 표준화하는 게 실질적 이득이 컸다.
그래서 어느 게 정답이냐
정답은 없다. 다만 "재사용하고 싶다"고 결정한 뒤에 스스로에게 물어볼 질문은 있다. "이건 스텝인가, 잡인가?" 이 한 문장이면 90%의 케이스는 결정된다.
물론 여전히 애매한 경우는 남는다. 예를 들어 "테스트 스텝 하나만 다르게 하고 싶은데 나머지는 같은 파이프라인"이 필요할 때, Reusable workflow의 input으로 test 커맨드를 받을 것인가, 아니면 파이프라인 안에서 특정 부분만 Composite action으로 오버라이드할 것인가. 여기서는 지금도 케이스마다 다르게 결정한다. 완벽한 규칙은 없다. 혹시 여러분 팀은 어떤 기준으로 나누시는지 궁금하다. 특히 monorepo 환경에서는 또 다른 트레이드오프가 있을 것 같은데, 나는 아직 그 경험이 얕다.