90초에서 2초로: 나의 Docker 빌드 속도 최적화 분투기

전체 서버 API 프로그램에 대해 정리를 하고, 작업했던걸 테스트 해보려고 Docker Compose로 프로젝트를 빌드하다 흥미로운 메시지를 발견했다.

Compose can now delegate builds to bake for better performance. To do so, set COMPOSE_BAKE=true.

성능 개선이라는 말에 COMPOSE_BAKE=true 옵션을 바로 적용했다. 이는 약 2만 줄 규모의 NestJS API 서버의 빌드 시간은 항상 90초를 훌쩍 넘기며 어우 너무 길다고 느꼈었다. 그러니 기대도 해봤지만… 결과는 생각보다 실망스러웠다. 아주 약간의 개선은 있었지만, 기대했던 드라마틱한 변화는 없었다.

이 글은 그 실망스러운 결과에서 시작하여, 근본적인 원인을 파헤치고 마침내 빌드 시간을 2초 내외로 단축하기까지의 과정을 정리한 글이다.

1. 첫 시도와 실망: 10%의 미미한 개선

COMPOSE_BAKE=true 옵션을 적용한 첫 빌드 시간은 약 88초였다. 기존 98초에 비하면 약 10% 빨라진 셈이니 효과가 없지는 않았다. 이 옵션의 경우 새로운 빌드 기능인데, 빌드 시 이미지를 만드는 과정에서 병렬 처리를 통해 성능이 아주 좋아질 거라고 그렇게 이야기 했었다. 실제로 사용하는 현재 개발 기기는 M4 맥북 프로, 램도 24기가바이트 이니 분명 넉넉하고도 남을 상황이었고, 그러면 못해도 3-40 % 개선이 있을 수 있다고 예상했던 것인데…

무언가 더 근본적인 문제가 내 빌드 과정에 숨어있음을 직감했다.

Bake라는 도구는 죄가 없었다. 문제는 내부에 있었다.

2. 근본 원인 추적: 왜 내 빌드는 항상 느렸을까?

결론적으로, 내 빌드 속도를 저해하던 범인은 세 가지였다.

  1. 잘못된 습관: ‘안전’을 맹신한 캐시 삭제 가장 큰 문제였다. 3.0.0 을 구성하면서 나는 빌드 시 클린 빌드가 되는 것이 가장 중요하다고 생각했다. ‘혹시 모를 충돌’이나 ‘깨끗한 빌드’를 명분으로, 매번 빌드 전에 docker rmi 명령어로 이전 이미지를 삭제하는 것을 package.json에서 수행하도록 자동화 했었다. 이것은 Docker의 가장 강력한 무기인 캐시(Cache)를 스스로 내다 버리는 행위였다.

  2. 비효율적인 Dockerfile: 잘못된 COPY 순서 두 번째 문제는 Dockerfile 내부에 있었다. 나는 습관적으로 소스 코드 전체를 복사(COPY . .)한 뒤에 의존성을 설치(RUN yarn install)했다. 이로 인해 소스 코드 단 하나만 수정해도 yarn install 캐시가 무효화되어 매번 수 분이 걸리는 의존성 설치를 반복하고 있었다.

  3. 나의 무지: COMPOSE_BAKE의 본질 Bake는 여러 빌드 작업을 병렬로 처리해주는 도구이지, 단일 작업 자체를 마법처럼 빠르게 만들어주는 도구가 아니었다. 내 빌드 시간의 대부분은 yarn install이라는 단일 네트워크 작업이 차지하고 있었으므로, Bake가 활약할 무대 자체가 없었던 것이다.

3. 전환점: 문제 해결과 최적화 적용

원인을 알았으니 해결은 명확했다. 각 문제를 해결하기 위해 적용한 구체적인 코드 변경 사항은 다음과 같다.

해결 1: Dockerfile 캐시 구조 최적화

Dockerfile의 빌더 스테이지에서 COPY 명령어의 순서를 조정하여 캐시 효율을 극대화했다.

  • 수정 전:

    # ❌ 문제 지점: 소스 코드를 의존성 설치 전에 복사
    COPY src ./src/
    RUN yarn install # 캐시가 거의 항상 무효화됨
    
  • 수정 후:

    # ✅ 개선 지점: 의존성 설치를 먼저 실행하여 캐시 레이어 생성
    RUN yarn install
    # ✅ 개선 지점: 자주 변경되는 소스 코드를 나중에 복사
    COPY src ./src/
    

해결 2: 파괴적인 빌드 스크립트 수정

unset 명령어를 빌드 과정에서 완전히 분리하고, 캐시 사용 여부에 따라 스크립트 역할을 명확히 나누었다.

  • 수정 전:

    { "build:dev": "yarn unset:dev && docker compose ... build" }
    
  • 수정 후:

    {
      "build:dev": "docker compose ... build",
      "build:dev:no-cache": "docker compose ... build --no-cache"
    }
    

4. 결과: 98%의 시간 단축, 개발 경험의 혁명

모든 최적화를 적용한 결과는 역시나 훌륭했다.

  • 최적화 이전 (캐시 X, 비효율 구조): 약 98초
  • 최적화 이후 (캐시 X, 효율 구조): 약 88초 (기존 대비 10% 향상)
  • 최적화 이후 (캐시 O, 효율 구조): 약 2초 (최초 대비 98% 시간 단축)

지표로 만들어보면?

이 시간 기록을 기반으로, 평균 프로젝트에서 개발 과정에서 빌드 하는 횟수를 평균 낸 적이 있는데, 이를 기준으로 단축이 실제로 얼마나 큰 차이를 만드는지 프로젝트 규모에 따라 예상해보았다.

기능 규모 예상 빌드 횟수 최적화 전 누적 시간 최적화 후 누적 시간 절약된 시간
소형 약 10회 약 16분 약 20초 약 15분
중형 약 100회 약 2시간 40분 약 3분 20초 약 2시간 36분
대형 약 250회 약 6시간 45분 약 8분 20초 약 6시간 37분 (거의 하루)

결과적으로, 대형 기능 하나를 개발할 때마다 거의 하루에 가까운 근무 시간을 절약할수 있게되었다. 이것은 단순한 시간 단축을 넘어, 잦은 빌드 대기로 인해 끊기던 개발의 리듬과 흐름(Flow)을 온전히 유지할 수 있게 만들었다.

5. 결론: 내가 얻은 교훈들

이번 최적화 과정을 통해 몇 가지 중요한 교훈을 얻었다.

  1. 도구를 의심하기 전에 나를 의심하라. Bake는 훌륭한 도구였지만, 내가 그 성능을 발휘할 환경을 만들어주지 못했다.
  2. Docker의 기본, 캐시를 믿고 활용하라. Dockerfile의 명령어 순서는 단순한 순서가 아닌, 캐시 전략 그 자체이다.
  3. 의도에 맞는 정확한 명령어를 사용하라. docker rmibuild --no-cache는 비슷해 보이지만, 그 역할과 결과는 완전히 다르다.

깨끗한 구조가 꼭 완벽한 개발 상황을 유지해주는 것은 아니었다. 안정성을 최우선으로 생각했지만, 반대로 그렇게 됨으로서 얼마나 많은 손해를 본건지… 다음번 CI/CD 를 수행한다면, 그때엔 이러한 점들의 고려가 필수라고 생각되고, 이러한 점에서 AI 나 동료들과의 검증 절차를 반드시 넣고 만들어보리라 생각을 할 수 있었다.

흠 재밌는 정리였다.