도커 이미지와 레이어: 0.1초 만에 빌드되는 마법의 원리

“어? 방금 빌드 끝난 거예요?”

도커의 편리함을 맛본 뒤, 나는 팀장님 몰래 내 개발 환경을 조금씩 도커로 옮기기 시작했다. Dockerfile이라는 설계도를 작성하고 docker build 명령어를 쳤다.

처음에는 시간이 꽤 걸렸다. 우분투를 다운받고, 자바를 깔고, 내 프로젝트의 라이브러리(Jar)들을 다운받느라 5분 정도가 소요됐다. “역시 처음이라 오래 걸리는구나.”

그런데 코드 오타를 하나 수정하고 다시 빌드를 눌렀을 때, 나는 내 눈을 의심했다. 엔터를 치자마자 터미널에 Successfully built 메시지가 떴다. 걸린 시간은 고작 0.1초.

“설마 고장 난 건가? 아까는 5분 걸리던 게 어떻게 순식간에 끝나지?”

불안한 마음에 서버를 띄워봤지만, 코드는 완벽하게 수정되어 있었다. 도대체 도커 안에서는 무슨 일이 벌어지고 있는 걸까? 이 미친 속도의 비밀은 바로 ‘레이어(Layer)’라는 독특한 저장 방식에 있었다.

도커는 처음부터 다시 짓지 않는다. 바뀐 블록만 교체할 뿐이다.

이미지(Image)와 컨테이너(Container)

본격적인 원리를 파기 전에, 헷갈리는 용어 두 개만 정리하고 가자.

  • 이미지 (Image): 실행 가능한 ‘환경’을 꽁꽁 얼려놓은 설계도이자 원본 도장이다. (읽기 전용)
  • 컨테이너 (Container): 도장을 꽝 찍어서 만들어낸 실체다. 실제로 돌아가는 프로그램이다. (쓰기 가능)

우리가 배포할 때 서버에 보내는 것은 ‘컨테이너’가 아니라 ‘이미지’다. 서버는 이 이미지를 받아서 실행(Run)만 하면 된다.

레이어(Layer): 투명 셀로판지의 마법

도커 이미지는 하나의 통짜 파일이 아니다. 마치 ‘투명 셀로판지(Layer)’를 여러 장 겹쳐 놓은 것과 같다. Dockerfile에 적힌 명령어 한 줄 한 줄이 각각 하나의 층(Layer)이 된다.

예를 들어보자.

# 1층: 우분투(OS)를 깐다.
FROM ubuntu:20.04

# 2층: 자바를 설치한다.
RUN apt-get install openjdk-17-jdk

# 3층: 내 코드를 복사한다.
COPY my-app.jar /app/my-app.jar

# 4층: 실행한다.
CMD ["java", "-jar", "/app/my-app.jar"]

Dockerfile에 적힌 내용들을 찬찬히 뜯어보면 구조가 그렇게 어렵지 않다는 것을 알 수 있다. “우분투를 깔고(FROM), 자바를 설치하고(RUN), 내 코드를 가져와서(COPY), 실행해라(CMD).”

결국 우리가 VM이나 서버 터미널에 접속해서 손으로 일일이 치던 ‘서버 세팅 과정’을 그대로 문서로 옮겨 적은 것뿐이다. 이 문서 한 장만 있으면, 누가 시키지 않아도 도커가 알아서 착착 설치를 진행한다. (이 개념은 나중에 우리가 다룰 ‘CI/CD 자동화’의 핵심 원리와도 연결된다.)

그런데 도커는 왜 이 과정을 한 번에 퉁치지 않고, 굳이 한 줄 한 줄 ‘레이어’로 나눠서 차곡차곡 쌓는 걸까?

바로 ‘재사용(Caching)’ 때문이다. 만약 내가 코드를 수정해서 다시 빌드한다면? 도커는 똑똑하게 생각한다. “1층(OS)이랑 2층(자바)은 아까랑 똑같네? 그럼 새로 안 만들고 아까 만들어둔 거 그대로 써야지(Cache Use).”

그리고 변경된 3층(코드 복사)부터만 새로 만든다. 그래서 두 번째 빌드가 0.1초 만에 끝난 것이다.

변하지 않는 부분은 얼려두고(캐싱), 변한 부분만 새로 굽는다.

[Code Verification] 레이어 캐시 눈으로 확인하기

이게 진짜인지 코드로 확인해보자. 간단한 Dockerfile을 만들고 두 번 빌드해보면 된다.

  1. Dockerfile 작성:
FROM alpine:latest
RUN echo "1. 기본 유틸 설치 중..." && sleep 2 # 2초 걸리는 작업
COPY test.txt /app/test.txt
CMD ["cat", "/app/test.txt"]

  1. 첫 번째 빌드:
$ docker build -t my-test:v1 .
# 결과:
# [2/3] RUN echo "1. 기본 유틸 설치 중..." ... 2.1s (2초 걸림)

  1. test.txt 내용 수정 후 두 번째 빌드:
$ docker build -t my-test:v2 .
# 결과:
# [2/3] RUN echo "1. 기본 유틸 설치 중..." ... CACHED (0초!)

로그에 선명하게 찍힌 CACHED라는 단어가 보이는가? 도커가 2초 걸리는 작업을 건너뛰었다는 증거다.

실무 조언: 순서가 생명이다

이 레이어 캐싱 원리를 알면, Dockerfile을 어떻게 짜야 하는지 답이 나온다. 핵심은 “잘 안 변하는 것을 아래(먼저)에, 자주 변하는 것을 위(나중)에 둬야 한다”는 것이다.

나쁜 예:

# 1. 소스 코드를 먼저 복사함 (자주 변함!)
COPY . . 
# 2. 라이브러리 설치 (거의 안 변함)
RUN npm install

소스 코드는 하루에도 수십 번 바뀐다. 1번이 바뀌면 도커는 그 뒤에 있는 2번(라이브러리 설치) 캐시도 다 깨버리고 다시 실행한다. 코드 한 줄 고쳤는데 npm install을 또 기다려야 한다.

좋은 예:

# 1. 라이브러리 목록(package.json)만 먼저 복사
COPY package.json .
# 2. 라이브러리 설치 (이러면 소스 코드가 바껴도 얘는 캐시됨!)
RUN npm install
# 3. 소스 코드 복사
COPY . .

이렇게 순서만 바꿔도 빌드 속도가 10배 빨라진다. 이것이 도커 고수와 초보의 차이다.

실무 조언 2: 이름표(Tag)는 생명줄이다

위 코드에서 docker build -t my-test:v1 .이라고 썼는데, 여기서 콜론(:) 뒤에 붙은 v1이 바로 태그(Tag)다.

많은 초보자가 귀찮다고 태그를 안 붙이는데, 그러면 도커는 자동으로 latest라는 태그를 붙인다.

  • my-test:latest: 가장 최신 버전이라는 뜻이다.
  • 문제점: 매번 latest로만 덮어쓰면, 서버에서 에러가 났을 때 “어? 어제 잘 되던 그 버전으로 돌려줘!”가 불가능하다. 이미 덮어써서 사라졌으니까.
  • 해결책: 반드시 v1, v2 혹은 날짜-시간처럼 고유한 태그를 붙여서 빌드해야 한다. 그래야 사고가 터졌을 때 v2를 버리고 v1으로 1초 만에 롤백할 수 있다.

이 태그 습관 하나가 나중에 당신의 퇴근 시간을 지켜줄 것이다.

마치며: 0.1초의 마법을 손에 넣다

도커 이미지는 단순한 파일 덩어리가 아니라, 가장 효율적으로 환경을 배달하기 위해 겹겹이 쌓아 올린 기술의 결정체다. 레이어 캐싱을 이해하고 Dockerfile 순서만 잘 맞춰도, 우리는 0.1초 만에 빌드되는 가볍고 빠른 배포 환경을 구축할 수 있다.

이제 내 컴퓨터의 환경을 완벽하게 얼려서(Image) 서버로 보낼 준비는 끝났다.

하지만 막상 서버에 이 이미지를 띄우려고 보니 새로운 고민이 생긴다. 웹 서비스는 딸랑 서버 컨테이너 하나로 돌아가지 않는다. 데이터베이스도 있어야 하고, 레디스도 있어야 하고, 프론트엔드 서버도 있어야 한다.

이 수많은 컨테이너를 일일이 하나씩 명령어로 켜고 끄려면 관리가 될까? 서로 통신은 어떻게 하지? 단일 ‘이미지’를 넘어, 거대한 ‘애플리케이션’을 구성하는 방법. 다음 시간에는 도커의 세계관을 확장하는 컨테이너, 서비스, 그리고 스택의 개념에 대해 알아보자.

“도커 이미지와 레이어: 0.1초 만에 빌드되는 마법의 원리”에 대한 1개의 생각

  1. 해당 블로그 게시글의 내용을 바탕으로, 실무적인 관점에서 보완 및 반박할 수 있는 내용을 정리해 드립니다. 댓글 형식에 맞춰 정중하면서도 기술적인 통찰을 담았습니다.

    [댓글 초안]
    게시글 잘 읽었습니다! 도커 이미지 레이어와 캐싱의 기본 원리를 아주 명확하게 설명해 주셨네요. 다만, 실무 운영과 최적화 관점에서 독자들이 함께 고려하면 좋을 몇 가지 **’반전 포인트’**가 있어 의견을 보탭니다.

    1. “레이어 통합은 항상 정답이 아니다”
    글에서는 레이어 수를 줄이는 것을 권장하지만, 실무에서는 의도적으로 레이어를 분리해야 할 때가 많습니다. 모든 명령을 &&로 묶어버리면 소스 코드 한 줄만 수정해도 거대한 레이어 전체의 캐시가 깨져 빌드 시간이 폭증하기 때문입니다. ‘캐시 적중률’을 높이려면 자주 변하는 것과 변하지 않는 것을 분리하는 레이어 설계가 더 중요합니다.

    2. “.dockerignore와 순서의 마법”
    레이어 구조를 이해하는 것보다 실무에서 더 크리티컬한 것은 COPY . .의 위치입니다. 의존성 파일(package.json이나 requirements.txt)만 먼저 COPY해서 빌드하고, 소스 코드는 나중에 복사하는 **’빌드 순서 최적화’**가 병행되지 않으면 본문의 캐싱 원리는 힘을 발휘하기 어렵습니다.

    3. “이미지 크기보다 중요한 건 멀티 스테이지 빌드”
    단일 스테이지에서 레이어를 줄이려고 애쓰는 것보다, Multi-stage Build를 사용하는 것이 현대적인 정석입니다. 빌드 환경과 실행 환경을 분리하면, 본문에서 언급된 ‘레이어 쌓기’ 고민을 넘어 최종 이미지 용량을 90% 이상 획기적으로 줄이고 보안성까지 확보할 수 있습니다.

    4. “Storage Driver의 오버헤드”
    레이어가 너무 깊어지면(Stacking) Overlay2 같은 스토리지 드라이버에서 파일 시스템 탐색 성능 저하가 발생할 수 있습니다. 단순히 용량 문제를 넘어 I/O 성능 관점에서도 적절한 레이어 깊이를 유지하는 균형 감각이 필요합니다.

    좋은 글 덕분에 도커의 기본기를 다시금 점검할 수 있었습니다. 위 내용들도 함께 갈무리된다면 더 완벽한 가이드가 될 것 같습니다!

    응답

댓글 남기기