도커 이미지와 레이어: 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) 서버로 보낼 준비는 끝났다.

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

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

댓글 남기기