이전 글에서 “내 PC에서만 되는” 재현성 문제를 짚었다. 이 문제를 정면으로 푸는 도구가 Docker이고, 그 핵심 개념이 컨테이너(container) 다. 이번 글에서는 컨테이너가 무엇인지, 가상 머신(VM)과 어떻게 다른지, 그리고 왜 컨테이너가 재현성을 보장하는지를 살펴본다.
컨테이너는 “코드 + 환경”을 통째로 담은 상자
컨테이너를 한 줄로 말하면, 애플리케이션 코드와 그 코드를 실행하는 데 필요한 모든 것(런타임, 라이브러리, 설정)을 하나로 묶어, 어디서든 똑같이 실행되도록 만든 격리된 실행 단위다.
비유하자면 해상 운송의 컨테이너 박스와 같다. 안에 무엇을 넣든(가전제품이든 농산물이든) 박스의 규격은 동일하므로, 어떤 배·트럭·항구에서도 같은 방식으로 다룰 수 있다. 소프트웨어 컨테이너도 마찬가지로, 안에 든 프로그램이 무엇이든 표준화된 하나의 단위로 포장되어 어느 환경에서나 동일하게 실행된다. Docker의 고래 로고가 컨테이너를 잔뜩 실은 모습인 것도 이 비유에서 왔다.
여기서 두 가지 용어를 구분해야 한다.
- 이미지(image): 컨테이너를 만들기 위한 템플릿(설계도). 코드, 라이브러리, 설정이 모두 담긴 읽기 전용 패키지다. 한 번 만들어 두면 변하지 않는다.
- 컨테이너(container): 이미지를 실행한 실체(instance). 이미지라는 설계도로 찍어낸, 실제로 돌아가는 프로세스다.
객체지향 프로그래밍에 익숙하다면, 이미지는 class, 컨테이너는 instance라고 생각하면 정확하다. 하나의 이미지로 똑같은 컨테이너를 여러 개 띄울 수 있다.
가상 머신(VM)과 무엇이 다른가
“환경을 통째로 담는다”는 말을 들으면 가상 머신(Virtual Machine, VM) 을 떠올릴 수 있다. VM도 격리된 환경을 제공하지만, 동작 방식이 근본적으로 다르다. 차이는 OS를 통째로 포함하느냐에 있다.
가상 머신(VM) 컨테이너(Container)
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ App │ │ App │ │ App │ │ App │ │ App │ │ App │
├─────┤ ├─────┤ ├─────┤ ├─────┤ ├─────┤ ├─────┤
│Guest│ │Guest│ │Guest│ │ libs│ │ libs│ │ libs│
│ OS │ │ OS │ │ OS │ └─────┘ └─────┘ └─────┘
└─────┘ └─────┘ └─────┘ ┌───────────────────────┐
┌───────────────────────┐ │ Docker Engine │
│ Hypervisor │ ├───────────────────────┤
├───────────────────────┤ │ Host OS │
│ Host OS │ ├───────────────────────┤
├───────────────────────┤ │ Hardware │
│ Hardware │ └───────────────────────┘
└───────────────────────┘
-
VM: 물리 하드웨어 위에 hypervisor가 있고, 그 위에 각 VM이 자기만의 게스트 OS(Guest OS) 를 통째로 올린다. 격리 수준이 매우 높지만, OS를 통째로 부팅해야 하므로 무겁고(보통 수 GB), 켜지는 데 수십 초~수 분이 걸린다.
-
컨테이너: 게스트 OS가 없다. 모든 컨테이너가 호스트 OS의 커널(kernel)을 공유하고, 그 위에서 각자 필요한 라이브러리만 격리해 올린다. OS를 새로 부팅하지 않으므로 가볍고(보통 수십~수백 MB), 거의 즉시(수 초 내) 뜬다.
| 가상 머신(VM) | 컨테이너(Container) | |
|---|---|---|
| 격리 단위 | OS 전체 | 프로세스 |
| OS | 게스트 OS를 각자 포함 | 호스트 OS 커널 공유 |
| 크기 | 수 GB | 수십~수백 MB |
| 시작 속도 | 수십 초~분 | 수 초 이내 |
| 한 서버에 띄우는 수 | 보통 수 개 | 수십~수백 개 |
핵심은 컨테이너가 OS를 복제하지 않고 커널을 공유하기 때문에 훨씬 가볍다는 점이다. 덕분에 한 서버에 컨테이너를 빽빽하게 올릴 수 있고, 이는 뒤에서 다룰 Kubernetes의 대규모 운영을 가능하게 하는 토대가 된다.
참고: 컨테이너는 리눅스 커널 기능(namespace로 격리, cgroup으로 자원 제한)에 기반한다. 그래서 컨테이너는 본질적으로 리눅스 기술이다. Windows나 macOS에서 Docker를 돌릴 때 내부적으로 가벼운 리눅스 VM이 하나 깔리고 그 위에서 컨테이너가 도는 이유가 이것이다.
왜 컨테이너가 재현성을 보장하는가
컨테이너가 “어디서나 똑같이 돈다”고 말할 수 있는 이유는, 코드가 의존하는 환경 전체를 이미지 안에 고정(freeze) 하기 때문이다.
내 PC에서 직접 pip install 하던 방식은, 실행 시점의 시스템 상태(이미 깔린 패키지, OS 버전, 환경 변수)에 의존한다. 그래서 다른 환경으로 옮기면 그 상태가 달라져 깨진다.
반면 이미지는 어떤 베이스 OS 위에, 어떤 버전의 어떤 패키지를, 어떤 순서로 설치했는지를 모두 포함한 채로 한 번 빌드되어 고정된다. 컨테이너는 매번 이 고정된 이미지로부터 새로 시작하므로,
- 내 PC, 동료의 노트북, 클라우드 서버 어디서 띄워도 컨테이너 안의 환경은 비트 단위로 동일하고,
- 바깥 호스트 환경이 무엇이든(컨테이너가 호스트 커널만 공유할 뿐 라이브러리는 격리되므로) 영향을 받지 않는다.
즉 컨테이너는 “환경을 코드처럼 고정해 함께 배포한다”. 이것이 재현성의 본질이다. 환경을 어떻게 이미지로 고정하는지, 즉 Dockerfile로 이미지를 빌드하는 법은 다음 글에서 다룬다.
요약
- 컨테이너는 코드와 실행 환경(라이브러리·설정)을 하나로 묶은, 어디서나 동일하게 실행되는 격리된 단위다.
- 이미지는 템플릿(class), 컨테이너는 그것을 실행한 실체(instance)다. 하나의 이미지로 여러 컨테이너를 띄울 수 있다.
- VM은 게스트 OS를 통째로 올려 무겁지만, 컨테이너는 호스트 OS 커널을 공유해 가볍고 빠르다.
- 컨테이너는 의존 환경 전체를 이미지에 고정하므로, 호스트 환경과 무관하게 동일하게 동작한다 → 이것이 재현성의 근원이다.