개발
home
🚀

BFF 배포 파이프라인(CD) 속도 개선기 — 7분 53초에서 3분 30초로

Created
2026/06/25
Tags
Docker
성능 최적화
node
2026-06-25

개요

매주 반복되는 BFF 배포가 평균 7분 53초나 걸렸습니다. 한두 줄 고쳐 올리는데도 8분 가까이 기다려야 했죠.
CD 로그를 뜯어보니 원인은 분명했습니다. 산출물에 영향도 주지 않는 단계가 러너에서 그대로 돌고 있었고, Docker 레이어 캐시는 깔려 있긴 했지만 거의 적중하지 못하고 있었습니다.
이 글은 BFF CD를 세 단계에 걸쳐 7분 53초 → 약 3분 30초, 약 56% 단축한 과정을 정리합니다. 핵심은 세 가지입니다.
러너에서 도는 중복 install/compile 제거
Dockerfile 의존성 설치 레이어 캐싱
멀티아키텍처 빌드의 레지스트리 캐시 태그 분리
본문에 들어가기 전에, 실제 CD 실행 기록부터 보겠습니다. 아래에서 위로(오래된 순 → 최신) 올라올수록 빌드 시간이 어떻게 줄었는지 한눈에 들어옵니다.
그림. 맨 아래 7분대(개선 전)에서 시작해 ① 러너 중복 제거로 5분대, ② 레이어 캐싱으로 3분대까지 내려갔다가, 두 아키텍처가 캐시를 서로 덮어써 잠깐 5분대로 튀고, ③ 캐시 태그를 분리한 뒤 3분대로 안정화됩니다. 빨간 주석이 각 전환점입니다.

파이프라인 구조

본격적으로 들어가기 전에 BFF CD가 어떻게 생겼는지 짚고 갑니다.
BFF는 멀티아키텍처(amd64 + arm64) 이미지를 배포합니다. 그래서 빌드 job이 두 개입니다.
build-amd64 — x86 러너
build-arm64 — ARM64 러너
merge-images — 두 결과를 docker buildx imagetools create로 하나의 manifest로 합쳐 push
한 번의 실행을 들여다보면 구조가 그대로 드러납니다. build-amd64build-arm64가 별도 job으로 병렬 실행되고, 둘 다 끝나면 merge-images가 두 이미지를 하나의 멀티아키텍처 manifest로 합칩니다. 그래서 전체 CD 시간은 결국 느린 쪽 빌드 job의 시간에 좌우됩니다.

본문

문제 1: 러너와 컨테이너가 같은 일을 두 번 한다

원래 각 빌드 job에는 이런 러너 단계가 들어 있었습니다.
- name: Enable Corepack run: corepack enable - uses: actions/setup-node@v4 with: node-version: 24 cache: yarn - name: Install dependencies run: yarn install --immutable - name: Build run: yarn compile
YAML
복사
문제는 바로 다음 단계인 docker buildx buildDockerfile 안에서 똑같은 일을 다시 한다는 것이었습니다. 컨테이너 빌드가 corepack enable, yarn install --immutable, compile을 자기 안에서 처음부터 수행하거든요.
즉 러너에서 깐 node_modules와 컴파일 산출물은 이미지에 들어가지도 못합니다. Docker는 빌드 컨텍스트를 COPY로만 가져가기 때문에, 러너에서 한 작업은 그대로 버려졌습니다.
그래서 네 단계(Corepack / setup-node / install / build)를 전부 제거했습니다. dev·staging·prod 세 환경의 워크플로우에 동일하게 적용했습니다.
결과: 7분 53초 → 5분 19초, 약 2분 34초(33%) 단축. 빌드 job의 critical path에서 군더더기 install/compile이 통째로 사라졌습니다.

문제 2: 소스 한 줄만 바꿔도 yarn install이 처음부터 돈다

다음 타깃은 Dockerfile이었습니다. 원래 이런 순서였습니다.
COPY . . RUN yarn install --immutable RUN yarn workspace bff compile
Docker
복사
COPY . .yarn install보다 위에 있는 게 함정입니다. 소스코드 한 줄만 바뀌어도 COPY 레이어 캐시가 깨지고, Docker 레이어 캐싱 특성상 그 아래 install 레이어까지 전부 무효화됩니다. 의존성은 그대로인데 매 배포마다 yarn install을 처음부터 다시 돌린 셈이죠.
Docker 레이어 캐싱의 기본은 자주 안 바뀌는 것을 위로, 자주 바뀌는 것을 아래로 두는 것입니다. 그래서 매니페스트만 먼저 복사하도록 순서를 바꿨습니다.
# 매니페스트만 먼저 복사 → deps 미변경 시 install 레이어 재사용 COPY package.json yarn.lock .yarnrc.yml ./ COPY .yarn/ .yarn/ COPY packages/bff/package.json packages/bff/ COPY packages/shared/package.json packages/shared/ ARG TARGETARCH RUN --mount=type=cache,id=yarn-cache-$TARGETARCH,sharing=locked,target=/usr/local/share/.cache/yarn \ yarn install --immutable COPY . . RUN yarn workspace bff compile
Docker
복사
이제 package.json / yarn.lock이 그대로인 커밋이면 install 레이어를 레지스트리 캐시에서 통째로 재사용합니다. 모노레포라 워크스페이스마다 package.json을 각각 복사해줘야 yarn이 워크스페이스 구조를 올바르게 인식합니다.
추가로 --mount=type=cache로 yarn 다운로드 캐시까지 아키텍처별($TARGETARCH)로 분리해 BuildKit 캐시 마운트도 활용했습니다.
결과: 캐시가 적중하면 3분 48초 ~ 3분 56초까지 떨어졌습니다. 다만 의존성이 추가된 커밋은 최초 1회 캐시 미스로 약 5분 20초가 걸립니다 — 이건 정상이고, 다음 배포부터 다시 캐시를 탑니다.

문제 3: amd64와 arm64가 같은 캐시를 서로 덮어쓴다

그런데 레이어 캐싱을 넣은 뒤에도 시간이 이상하게 출렁였습니다. 3분 후반대로 잘 나오다가 갑자기 5분대로 튀어오르길 반복했죠. 로그를 보며 "시간이 왜 5분으로 늘었지?"를 한참 들여다봤습니다.
원인은 멀티아키텍처 빌드의 캐시 태그였습니다. 빌드 job이 amd64·arm64 두 개인데, 둘 다 같은 레지스트리 캐시 태그를 공유하고 있었습니다 — 그것도 :cache 하나로.
--cache-from type=registry,ref=...:cache --cache-to type=registry,mode=max,ref=...:cache,...
Bash
복사
amd64가 푸시한 캐시를 arm64가 덮어쓰고, 그 반대도 일어나면서 서로의 캐시를 무효화했습니다. 애초에 아키텍처별 레이어는 호환되지도 않는데 한 태그를 공유한 게 잘못이었죠. 그래서 캐시 미스가 난 쪽이 다시 5분대로 올라간 겁니다.
태그를 아키텍처별로 분리했습니다.
# amd64 job --cache-from ...:cache-amd64 --cache-to ...:cache-amd64 # arm64 job --cache-from ...:cache-arm64 --cache-to ...:cache-arm64
Bash
복사
이제 두 아키텍처가 각자의 캐시 저장소를 쓰니 상호 덮어쓰기가 사라졌습니다.
결과: 캐시 적중 시 3분 9초 ~ 3분 35초로 안정화. 더 이상 5분대로 튀지 않습니다.

정리

세 단계를 거치며 BFF CD는 이렇게 줄었습니다.
단계
CD 시간
비고
개선 전 (평균)
7분 53초
매 배포마다 install·compile·빌드를 풀로 수행
① 러너 중복 제거
5분 19초 (-33%)
산출물에 영향 없는 단계 삭제
② 레이어 캐싱
3분 48초
캐시 적중 시 (의존성 변경 시 최초 1회 ~5분 20초)
↳ 출렁임
5분대로 회귀
amd64·arm64 캐시 상호 덮어쓰기
③ 캐시 태그 분리
3분 30초 안팎
캐시 적중 안정화
세 번의 변경으로 배포 시간을 7분 53초에서 3분 30초, 절반 넘게 줄였습니다. 비결은 직관적입니다 — 안 해도 되는 일은 빼고(중복 단계 제거), 한 번 한 일은 다시 쓰게 만들고(레이어 캐싱), 그 캐시가 확실히 적중하도록 맞췄습니다(태그 분리). 새 기술을 들인 게 아니라, 파이프라인이 실제로 무슨 일을 하는지 끝까지 파고든 결과입니다.
교훈 두 가지. ① CI가 느리면 "이 단계가 정말 필요한가"부터 의심하자 — 가장 큰 절감(33%)이 단순 단계 삭제에서 나왔다. ② 캐시는 '켜는' 게 아니라 '적중하게 만드는' 게 일이다. cache-from/to만 박아두고 적중률을 안 보면, 깔아둔 캐시가 매번 미스 나도 모른다.