[도커 30강] 07강. 레이어 캐시와 빌드 속도 최적화

[도커 30강] 07강. 레이어 캐시와 빌드 속도 최적화

도커를 쓰다 보면 초반에는 빌드가 금방 끝나서 체감이 잘 안 오지만, 프로젝트가 조금만 커져도 docker build 대기 시간이 팀 전체 생산성을 크게 깎아먹는 구간이 반드시 옵니다. 특히 CI에서 매 커밋마다 이미지를 다시 만드는 팀이라면, 빌드가 2분에서 8분으로 늘어나는 순간부터 리뷰-수정-검증 사이클 자체가 느려집니다. 오늘은 이 문제를 해결하는 핵심 도구인 **레이어 캐시(layer cache)**를 제대로 이해하고, 실제로 빌드 시간을 줄이는 구조를 익히겠습니다.

핵심은 단순합니다. 도커는 Dockerfile의 각 명령을 “레이어”로 저장하고, 이전에 같은 조건으로 실행된 레이어가 있으면 재사용합니다. 따라서 어떤 파일을 먼저 복사하고 어떤 명령을 먼저 실행하느냐가 곧 속도입니다. 즉, 같은 기능의 Dockerfile이라도 순서 설계에 따라 빌드 속도는 크게 달라집니다.


핵심 개념

  • 도커 빌드는 Dockerfile 명령 단위로 레이어를 만들고, 각 레이어의 입력값(명령, 파일 내용, 빌드 인자 등)이 같으면 캐시를 재사용합니다.
  • COPY . .를 초반에 두면 작은 코드 변경에도 이후 레이어가 연쇄적으로 무효화되어 전체 빌드가 느려집니다.
  • 변경 빈도가 낮은 단계(시스템 패키지 설치, 언어 런타임 의존성 설치)를 위로, 변경 빈도가 높은 단계(애플리케이션 소스 복사)를 아래로 배치하면 캐시 효율이 좋아집니다.
  • .dockerignore는 캐시 안정성의 핵심입니다. 빌드 컨텍스트에 불필요한 파일이 섞이면 해시가 자주 바뀌어 캐시를 깨뜨립니다.
  • BuildKit 기능(--mount=type=cache 등)을 쓰면 패키지 매니저 캐시까지 활용해 빌드 시간을 더 줄일 수 있습니다.

기본 사용

예제 1) 캐시를 일부러 깨뜨리는 느린 Dockerfile와 비교 기준 만들기

mkdir -p ~/docker100/lesson07/src && cd ~/docker100/lesson07
cat > requirements.txt <<'EOF'
flask==3.0.3
requests==2.32.3
EOF

cat > src/app.py <<'EOF'
from flask import Flask
app = Flask(__name__)

@app.get('/')
def hello():
    return 'lesson07 cache optimization'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
EOF
cat > Dockerfile.slow <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "src/app.py"]
EOF

time docker build -f Dockerfile.slow -t docker100-lesson07:slow .

설명:

  • COPY . /app가 먼저 오기 때문에, 코드 한 줄만 바뀌어도 RUN pip install ... 레이어까지 다시 실행될 가능성이 큽니다.
  • 첫 빌드는 빠르든 느리든 기준점입니다. 진짜 차이는 두 번째, 세 번째 빌드에서 드러납니다.
  • time으로 실제 소요 시간을 측정해 “감”이 아닌 데이터로 개선 여부를 확인하는 습관이 중요합니다.

예제 2) 캐시 친화적 Dockerfile로 재구성

cat > Dockerfile.fast <<'EOF'
FROM python:3.12-slim
WORKDIR /app

COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

COPY src/ /app/src/
CMD ["python", "src/app.py"]
EOF

time docker build -f Dockerfile.fast -t docker100-lesson07:fast .
echo "# cache test" >> src/app.py
time docker build -f Dockerfile.fast -t docker100-lesson07:fast .

설명:

  • 의존성 파일을 먼저 복사하고 설치하면, 코드 수정 시 pip install 레이어를 재사용할 가능성이 높습니다.
  • 즉, 자주 바뀌는 파일(src)은 뒤로 밀고, 잘 안 바뀌는 파일(requirements.txt)은 앞으로 당겨 캐시를 보호합니다.
  • 두 번째 빌드에서 레이어 재사용 로그(CACHED)가 많이 보이면 구조가 잘 잡힌 것입니다.

예제 3) .dockerignore로 캐시 흔들림 줄이기

cat > .dockerignore <<'EOF'
.git
__pycache__/
*.pyc
.env
.venv/
node_modules/
.DS_Store
*.log
EOF

time docker build -f Dockerfile.fast -t docker100-lesson07:fast .

설명:

  • .dockerignore가 없으면 .git, 로컬 캐시, 로그 파일까지 빌드 컨텍스트로 들어가 해시가 자주 변합니다.
  • 컨텍스트 변화가 크면 Dockerfile은 안 바꿨는데도 캐시 재사용률이 떨어질 수 있습니다.
  • 팀 단위에서는 .dockerignore를 “성능 + 보안 + 재현성” 기본 파일로 간주해야 합니다.

예제 4) BuildKit 캐시 마운트로 패키지 다운로드 최적화

cat > Dockerfile.buildkit <<'EOF'
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r /app/requirements.txt
COPY src/ /app/src/
CMD ["python", "src/app.py"]
EOF

DOCKER_BUILDKIT=1 time docker build -f Dockerfile.buildkit -t docker100-lesson07:buildkit .

설명:

  • BuildKit 캐시 마운트는 패키지 다운로드 캐시를 보존해 네트워크/저장소 I/O 부담을 줄입니다.
  • --no-cache-dir를 무조건 습관처럼 넣는 대신, 빌드 전략에 따라 BuildKit 캐시를 활용하는 것이 더 빠를 때가 많습니다.
  • CI 환경에서도 캐시 백엔드(예: registry cache)를 함께 설계하면 반복 빌드 시간이 눈에 띄게 감소합니다.

자주 하는 실수

실수 1) COPY . .를 상단에 두고 “도커가 원래 느리다”고 결론냄

  • 원인: Dockerfile 명령 순서를 성능 요소로 보지 않고, 기능만 맞으면 된다고 생각함.
  • 해결: 변경 빈도 기반으로 레이어를 재정렬합니다. 의존성 설치 단계는 위로, 소스 복사는 아래로 이동합니다.

실수 2) .dockerignore를 비워두거나 아예 만들지 않음

  • 원인: 로컬 개발 파일이 빌드 컨텍스트에 계속 섞여 들어가 캐시 키를 흔듦.
  • 해결: .git, .env, 캐시 디렉터리, 로그 등을 명시적으로 제외해 컨텍스트를 안정화합니다.

실수 3) 캐시가 깨졌는지 확인하지 않고 체감으로만 튜닝함

  • 원인: 빌드 로그를 읽지 않고 “이번엔 좀 빠른 것 같다” 수준에서 종료.
  • 해결: time docker build ...로 측정하고, 로그의 CACHED 비율을 확인해 객관적으로 비교합니다.

실수 4) 빌드 인자(ARG)나 시크릿 값이 자주 바뀌는 구조로 설계

  • 원인: 빈번히 변하는 값을 상단 레이어에서 소비해 하위 캐시를 연쇄 무효화.
  • 해결: 변동값을 최대한 아래 단계로 내리고, 비밀값은 BuildKit secret 사용 등으로 캐시 파괴를 최소화합니다.

실무 패턴

실무에서는 “한 번 빠른 빌드”보다 “매일 안정적으로 빠른 빌드”가 중요합니다. 그래서 개인 최적화보다 팀 규칙으로 고정하는 것이 효과가 큽니다.

첫째, Dockerfile 리뷰 체크리스트에 레이어 순서 검토를 넣습니다. 코드 리뷰에서 기능과 함께 “이 변경이 어떤 레이어 캐시를 깨는가?”를 반드시 확인하면, 성능 회귀를 초기에 막을 수 있습니다.

둘째, 언어별 의존성 파일을 분리하고 잠급니다. Python은 requirements.txt, Node.js는 package-lock.json, Java는 lock/manifest 파일을 먼저 복사해 설치 레이어를 고정합니다. 락 파일이 안정적일수록 캐시 적중률이 올라갑니다.

셋째, CI에서 캐시 전략을 명문화합니다. 로컬에서만 빠르고 CI에서는 매번 풀빌드라면 팀 체감은 개선되지 않습니다. BuildKit + 원격 캐시(레지스트리)를 사용해 러너가 바뀌어도 캐시 재사용이 가능하도록 구성해야 합니다.

넷째, 빌드 성능을 운영 지표처럼 다룹니다. “평균 빌드 시간”, “P95 빌드 시간”, “캐시 적중률”을 주간 단위로 보면 Dockerfile 퇴행을 빠르게 발견할 수 있습니다. 실제로 앱 코드보다 Dockerfile 변경 하나가 CI 시간을 배로 늘리는 경우가 흔합니다.

다섯째, 베이스 이미지 업데이트 전략을 분리합니다. 보안 업데이트는 중요하지만, 무계획 업데이트는 캐시 전체를 흔듭니다. 정기 업데이트 창(예: 주 1회)을 잡고, 그 외에는 불필요한 베이스 이미지 변경을 줄이면 개발 흐름이 안정됩니다.

여섯째, 멀티스테이지 빌드와 결합을 준비합니다. 7강에서는 캐시 기초에 집중했지만, 이후 18강 멀티스테이지와 결합하면 빌드 속도 + 이미지 크기 + 보안 면에서 동시에 이점을 얻을 수 있습니다. 즉 지금 배우는 레이어 감각은 뒤 강의의 전제입니다.

오늘의 결론

한 줄 요약: 도커 빌드 속도는 하드웨어보다 Dockerfile 순서 설계가 먼저이며, 레이어 캐시를 지키는 구조가 곧 팀 생산성이다.

“도커가 느리다”는 문제의 상당수는 기술 한계가 아니라 파일 배치와 캐시 설계 문제입니다. 오늘 배운 기준(변경 빈도, 컨텍스트 최소화, 측정 기반 튜닝)을 적용하면, 같은 프로젝트에서도 빌드 체감이 확실히 달라집니다. 다음 강의로 갈수록 Dockerfile이 길어지더라도, 캐시 관점으로 읽는 습관이 있으면 병목을 빠르게 찾고 개선할 수 있습니다.

연습문제

  1. Dockerfile.slowDockerfile.fast를 각각 2회 빌드해 두 번째 빌드 시간을 비교해보세요. 어떤 레이어에서 차이가 나는지 로그 기준으로 설명해보세요.
  2. .dockerignore를 적용하기 전/후로 빌드 컨텍스트 크기와 캐시 적중률 변화를 확인해보세요. 팀 프로젝트에 즉시 적용할 제외 항목 5개를 적어보세요.
  3. BuildKit 캐시 마운트를 켠 버전과 끈 버전의 의존성 설치 시간을 비교하고, CI에 적용할 때 주의할 점(캐시 보존, 보안, 러너 변경)을 정리해보세요.

이전 강의 정답

6강 연습문제 해설:

  • RUN은 빌드 시점에 실행되고 이미지 레이어로 남으며, CMD는 컨테이너 실행 시점의 기본 명령입니다. 따라서 설치/환경 준비는 RUN, 앱 시작은 CMD로 분리해야 재현성과 운영 안정성이 올라갑니다.
  • COPY . /app를 먼저 두는 Dockerfile은 코드 한 줄 수정에도 의존성 설치 레이어가 무효화되기 쉽고, requirements.txt를 먼저 복사해 설치하는 구조는 재빌드에서 캐시를 더 잘 재사용합니다.
  • docker run 이미지docker run 이미지 <다른 명령> 비교를 통해 CMD가 실행 시점에 덮어써질 수 있음을 확인할 수 있으며, 이는 디버깅에는 유용하지만 운영 표준 명령 남용을 막는 규칙이 함께 필요합니다.

실습 환경/재현 정보

  • OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
  • Docker 버전: Docker Engine / Docker Desktop 25.x 이상
  • 실행 순서:
    1. 실습 디렉터리와 샘플 앱 준비
    2. Dockerfile.slow 빌드 및 시간 측정
    3. Dockerfile.fast 빌드 후 코드 수정 재빌드
    4. .dockerignore 적용 전/후 비교
    5. BuildKit 캐시 마운트 버전 실험
  • 재현 체크:
    • 코드만 바꿨을 때 의존성 설치 단계가 캐시되는가?
    • 빌드 로그에 CACHED가 증가하는가?
    • .dockerignore 적용 후 불필요 컨텍스트가 제외되는가?
    • BuildKit 사용 시 반복 빌드 시간이 안정적으로 줄어드는가?