들어가며
솔직하게 지금까지는 Docker를 활용하기에 바빴다. Dockerfile로 image 관리해보고,, port 격리해보고,,, 아래의 명령어를 필요할때 써보기에 급급했다.
docker run
docker ps
docker logs
docker exec
...
하지만 조금 더 아래로 내려가 보면 Docker는 완전히 새로운 기술이라기보다는, Linux Kernel이 제공하는 여러 기능을 개발자가 사용하기 쉬운 형태로 묶어낸 도구에 가깝다.
최근에 컨테이너에 대해 Kernal 기반으로 조금 딥하게 들어가볼 기회가 생겨서 컨테이너가 어떤 Linux 기능 위에서 동작하는지, Docker Engine은 어떤 역할을 하는지, 그리고 Docker 구조가 왜 containerd, containerd-shim, runC로 나뉘게 되었는지 정리해보려 한다.
차근차근 가보자🔥
본문으로
1. 컨테이너 격리의 원천 기술 - Kernel
우선 그렇다면 컨테이너란 무엇일까? 아래 글을 보기 전에 다들 잠깐만 생각해보면 좋겟다!(나도 처음 들었을 때 명확하게 답하기 애매했다.)
전공시간에 Virtual Machine을 깔아본 적이 있는가?(VMware, Virtual Box) 깔아보았다면 아래사진처럼 하나의 HostOS(나는 맥북이라 MacOS다) 위에 여러 GuestOS(Ubuntu, Darwin ..)를 깔아서 그 위에 App을 실행해보는 것 까지가 보통 과제로 나온다...(굉장히 까다로웠던 기억이🥵)

그렇다면 컨테이너도 이렇게 매번 HostOS위에 Guest OS를 깔아서 그 위에 실행하는 것일까? -> 결론부터 말하자면 아니다!
컨테이너는 Kernel 기능을 이용해 HostOS의 프로세스 하나를 외부와 격리시킨 것 뿐이다. 아래 사진은 namespace, cgroups 등 다양한 이야기가 나오지만, 왼쪽 사진에서 Host OS위에 프로세스를 격리시켜 APP을 실행시키고 있는 것만 것만 확인해보자 ㅎㅎ.

그렇다면 여기서 나오는 namespace, cgroups 이런 내용들은 뭘까? -> 결론 먼저 말하자면 Linux의 namespace, cgroups, Unoin FileSystem 세 개의 기능을 활용하여 프로세스를 격리시킨다. -> 그것을 컨테이너라고 부르는 것이다!! 조금 더 자세히 알아보자.
1-1. namespace
namespace는 프로세스를 논리적으로 구분하는 커널 기능이다.
예를 들어 컨테이너 내부에서는 자기 자신이 독립된 PID 공간을 가진 것처럼 보일 수 있다. 하지만 실제로는 Host OS 위에서 실행되는 하나의 프로세스다. 자세한 내용은 아래 사진처럼 구성할 수 있다.

1-2. cgroups
cgroups는 프로세스(컨테이너)별 얼마나 리소스를 쓸 수 있는지 결정하는 커널의 기능이다. process는 메모리를 서로 나눠 점유하는 것이기에, 메모리를 너무 많이 먹으면 OS가 그 프로세스를 죽여버린다(Out Of Memory Kill), 많은 일을 할 컨테이너라면 좀 크게 할당해야하지 않겠는가!

1-3. Union FileSystem
Union FileSystem은 여러 레이러를 합쳐서 동작하게끔 해주는 리눅스 커널 기능이다. 아래에서 더 자세히 다룬다!

🍀 컨테이너는 새로 생긴 개념이 아니라, Linux의 namespaces, cgroups, Union FileSystem 같은 기능을 이용해 Host OS 위의 프로세스를 격리된 환경처럼 실행하는 기술이다 🍀
Docker는 이러한 Linux 기능들을 사용자가 직접 다루지 않아도 되도록 CLI와 워크플로우로 감싸서 제공했다. 그래서 개발자는 복잡한 Kernel 기능을 몰라도 docker run 같은 명령어로 컨테이너를 실행할 수 있게 되었다. (당시에는 매우 파격적이었다는!)
2. Container Packaging
Docker는 기본적으로 Client-Server 구조로 동작한다. 사용자가 터미널에서 Docker CLI 명령어를 입력하면, 이 요청은 Docker Daemon, 즉 Docker Engine으로 전달된다.

Docker Daemon -> Docker의 핵심 프로세스처럼 동작. 사용자의 명령을 받아 이미지를 빌드하거나, 컨테이너를 생성하고 실행하는 역할을 한다.
Local Image Store -> 로컬에 저장된 Docker Image들이 위치하는 공간
Containers -> 이미지를 기반으로 실제 실행 중인 컨테이너들이 존재하는 영역이다.
3. Container Sharing
Docker를 한번이라도 써봤다면, Dockerfile을 작성해봤을 것이다!

이러한 정적 파일을 가지고 이미지를 빌드하여 아래 사진처럼 사용한다.

아래처럼 플로우를 정리할 수 있겠다. (Rest형식으로 요청을 보낸다.)
docker build
↓
Docker Daemon이 요청 처리
↓
Local Image Store 확인
↓
없으면 Docker Hub에서 pull
↓
이미지를 기반으로 새로운 Image 생성
여태까지 Image를 빌드하고 쓰기만 급급했는데, 이렇게 자세한 플로우를 파악해놓으니 뭔가 속이 뻥 뚫린다 ㅎㅎ.
4. Container Architecture
이제 컨테이너의 개념에 대해서는 확실하게 말할 수 있을 것이다. 격리된 프로세스를 활용한 아키텍처는 아래처럼 발전해오기 시작했다.

이정도면 괜찮지 않을까? 싶은데 여기서 문제가 발생한다.
🚨 만약 다른 프로세스가 cgroups로 적절히 제한되어 있지 않고 너무 많은 메모리를 점유한다면 ?🚨
이때 Linux OOM(Out Of Memory) Killer가 Docker Engine을 죽일 수도 있다. Docker Engine도 결국 Host OS 위에서 실행되는 하나의 프로세스이기 때문이다.
기존의 구조는 Docker Engine이 컨테이너의 생성, 실행, 생명주기 관리까지 너무 강하게 책임지고 있었다.
당연하게도 Docker는 이를 분리하여 아래 사진과 같은 구조를 만들었다.

복잡한 용어들이 많이 나왔다. Docker가 어떻게 의존성을 분리했는지, 어디한번 알아보자.
Docker CLI
↓
Docker Engine (예전처럼 책임이 많지 않음)
↓
containerd
↓
containerd-shim (이 친구 덕분에 도커엔진이 죽어도 컨테이너는 살아있다!)
↓
runC
↓
container process
Docker Engine -> 사용자의 명령을 받고, 이미지, 네트워크, 볼륨 같은 Docker 사용자 경험을 담당한다. 예전처럼 실제 컨테이너 실행을 끝까지 직접 붙잡고 있지는 않는다.
containerd -> 컨테이너 생명주기를 관리하는 핵심 런타임 데몬이다. 컨테이너 생성, 시작, 중지, 이미지 pull, snapshot 관리 같은 더 낮은 수준의 컨테이너 관리를 담당한다.
containerd-shim -> 컨테이너 프로세스와 containerd 사이에 남아 있는 작은 중간 프로세스다. 이 친구 덕분에 Docker Engine이나 containerd가 재시작되더라도, 이미 실행 중인 컨테이너 프로세스가 바로 같이 죽지 않을 수 있다.
runC -> OCI 표준을 따르는 저수준 컨테이너 런타임이다. 실제로 Linux Kernel의 namespaces, cgroups, security 설정 등을 적용해서 컨테이너 프로세스를 생성한다.
여기서 재밌는 점은 여기서 중요한 점은 runC가 계속 떠 있는 데몬이 아니라는 것이다. runC는 컨테이너를 만들 때 잠깐 실행되어 다음과 같은 작업을 수행한다.
namespaces 설정
cgroups 설정
security 설정
컨테이너 프로세스 시작
컨테이너 프로세스가 실행되면 runC는 빠져나온다. (쏘옥~!) 그럼 보통 아래와 같은 느낌으로 컨테이너가 실행중일 것이다.
containerd-shim
└── nginx, java, mysql 같은 실제 컨테이너 프로세스
이제는 의존성을 분리했기에, 만약 Docker Engine이 죽는다면, Docker 명령어는 실행할 수 없겠지만 컨테이너는 살아있을 것이다!
Docker Engine 죽음
↓
Docker 명령어는 안 됨
↓
하지만 containerd-shim과 컨테이너 프로세스는 계속 살아있을 수 있음
Docker Engine 하나에 집중되어 있던 컨테이너 실행 책임을 containerd, containerd-shim, runC로 분리해서
Docker Engine 장애가 곧바로 컨테이너 장애로 이어지지 않도록 만든 것이다.
5. Container Image Layer
이 목차에서는 아키텍처보다는 도커 이미지에 자세히 다뤄본다.
다들 Docker Image에 대해 뭐라고 생각하는가? 나는 실행환경과 플로우를 담은 큰 파일 정도? 로 생각하고 있었다.
하지만 더 깊은 이해를 위해서는 Docker Image는 여러 Layer로 구성되어 있다는 것을 이해해야 한다. 아래사진처럼 Dockerfile을 작성해서 DockerHub에서 확인하면 Layer별로 나뉘어서 실행되는 것을 볼 수 있다.

또 아래처럼 읽기전용과 쓰기전용 Layer로 나뉘는데,
Image는 여러 Layer로 나뉘어도 모두가 수정 불가능한 읽기전용 Layer이다.

변경 사항은 쓰기 가능한 Container Layer에 기록된다. (우리가 실행(exec d) 로그(logs)를 볼때는 이 레이어에 쌓일 것이다)
그렇다면 이렇게 서로 다른 내용들이 각기 다른 레이어에 쓰여지고 쌓이는데,,, 우리는 도커 컨테이너를 확인할 때 구체적인 레이어로 들어가서 해당 내용을 확인하지 않는다! 어떻게 하나의 시스템처럼 로그를 합친거지? -> 이 내용은 맨 위에 나왔던 Linux의 Union FileSystem의 기능으로 해결한다.
좀 더 자세한 내용을 보자. 아래 커맨드 내용은 컨테이너의 해시코드를 docker inspect로 들어간 내용이다. 노란박스는 각각 layer를 나타낸다.
/localdir -> docekrfile을 참조하여 만든 가장 아래있는 디렉토리 -> 수정 불가
/upperdir -> 컨테이너를 만들고 새로 작성할 디렉토리 -> 수정가능 -> 휘발성
/overlay -> localdir과 upperdir를 합친 것 -> 이게 우리가 보는것. -> 이것을 보여주는 것이 Linux의 Union FileSysytem이다.

그래서 /upper에다가 soma라는 파일을 만들면, 컨테이너 안에도 파일이 생긴다.

아래 사진처럼 정리할 수 있겠다.

이 구조 덕분에 Docker는 저장 공간을 효율적으로 사용할 수 있다.
Ubuntu 같은 base image를 여러 컨테이너가 함께 사용하더라도, 공통 Layer는 재사용(Cache)하고 각 컨테이너에서 변경된 내용만 별도로 저장하면 된다.
이런 방식을 Copy-on-Write, 줄여서 CoW라고 한다.
읽을 때는 기존 Layer를 공유하고, 쓸 때만 변경된 내용을 새로운 Layer에 기록하는 방식이다.
6. Image Build Strategy
Docker Image가 Layer로 구성된다는 점과 CoW를 이해하면 Dockerfile을 어떻게 작성해야 하는지도 보인다. (그전에는 빌드 속도 빨라진다길래 블로그에 있던 거 따라했던 것 같다..!)
6-1. 지운파일도 Layer엔 남는다!
마지막 줄에서 파일을 삭제했더라도 삭제되었다고 표시만 되고, 이전 Layer에는 이미 해당 파일이 남아 있다. Docker Image는 Layer가 쌓이는 구조이기 때문이다. 설치와 정리는 가능하면 하나의 RUN 명령 안에서 처리하는 것이 좋다.


6-2. 캐싱을 잘 활용하자!
COPY . .는 현재 디렉토리 전체를 복사한다. 이렇게 하면 코드 한 줄만 바뀌어도 해당 Layer 이후의 캐시가 모두 깨질 수 있다.
특히 Java/Spring 프로젝트에서는 의존성 다운로드나 Gradle 설정처럼 무거운 작업은 자주 바뀌지 않는다. 반면 실제 소스 코드는 자주 바뀐다.
따라서 자주 바뀌지 않는 파일을 먼저 복사해 캐시를 활용하고, 자주 바뀌는 소스 코드는 뒤에서 복사하는 식으로 구성하는 것이 좋다.

6-3. base image가 가벼운 것을 이용하자.
base image는 가능한 가벼운 것을 선택하는 것이 좋다.

예를 들어 alpine 태그가 붙은 이미지는 용량이 작다.
FROM node:20-alpine
다만 alpine은 가벼운 대신 일반적인 Linux 배포판과 차이가 있다. 예를 들어 apt를 사용할 수 없고, 패키지 관리 방식도 다르다.
그래서 개발 중에는 필요한 패키지를 설치하기 쉬운 slim 계열 이미지를 사용하고, 운영 환경에서는 더 가벼운 alpine 계열 이미지를 고려할 수 있다. (distroless는 금융권에서 사용한다고 한다..!)
마무리하며
Docker를 단순히 명령어 중심으로 활용하다보니 여태까지는 내부 구조를 생각하지 않았다. 하지만 조금만 아래로 내려가 보면 Docker는 Linux Kernel 기능 위에서 동작한다는 것을 알 수 있다. (namespace, cgrous, Union FileSystem)
특히 Docker Image가 Layered로 나누어져 있는 것을 확실히 이해한 상태에서, Image Build 전략을 작성하니 -> 역시 CS지식이 안들어가는 곳이 없구나...라는 생각이 들면서 -> 결국 CS를 더 잘 공부해야겠다! 라는 결론이 지어지는 것 같다!!
어떤 기술을 쓰더라도 그 내부를 알아보자! 아자자!

'Infra' 카테고리의 다른 글
| [Infra] 모니터링 서버 도입 전에 할 수 있는 부하 테스트와 성능 병목 확인(Amazon CodeGuru Profiler + k6 스트레스 테스트) (0) | 2026.05.24 |
|---|---|
| [Infra] Prometheus와 Grafana로 우리 서비스의 개선점과 에러 찾기 (0) | 2026.05.23 |
| [Infra] 인스턴스 연결성 검사 실패 문제. 메모리가 부족하면 ssh 연결도 막힌다. (0) | 2026.02.21 |
| [Infra] Blue/Green 무중단 배포 (Docker, Nginx, GitHub Actions) 구현해보기 (5) | 2025.11.21 |