Docker? 그게 도대체 뭔데?
프롤로그: Docker를 마주하다
Spring Boot 기반의 TaskFlow 프로젝트를 진행하던 중, Docker라는 기술을 접하게 되었습니다. "Docker? 그게 뭔데?"라는 궁금증에서 시작된 학습 여정을 정리해보려 합니다.
첫 번째 질문: "Docker가 대체 뭔가요?"
처음 Docker를 접했을 때 가장 먼저 든 의문은 "Docker가 정확히 무엇인가?"였습니다.
Docker는 애플리케이션을 컨테이너로 패키징하는 기술입니다. 이를 이해하기 위해 이사용 컨테이너에 비유해보겠습니다.
기존 방식의 문제:
개발자 A의 컴퓨터: Java 17, MySQL 8.0, Ubuntu 22.04
개발자 B의 서버: Java 11, MySQL 5.7, CentOS 7
결과: "내 컴퓨터에서는 잘 되는데..." 현상 발생
Docker의 해결책:
Docker 컨테이너: [애플리케이션 + 실행환경] 모두 포함
결과: 어느 환경에서든 동일하게 실행
마치 이사할 때 모든 물건을 컨테이너에 넣어두면 어디로 이사를 가든 그대로 꺼내서 사용할 수 있는 것처럼, Docker는 애플리케이션과 실행 환경을 하나로 묶어서 어느 서버에서든 동일하게 실행할 수 있게 해줍니다.
두 번째 궁금증: "Docker는 어떻게 생긴 건가요?"
Docker의 장점은 이해했지만, 내부가 어떻게 구성되어 있는지 궁금했습니다.
Docker의 전체 아키텍처
내 컴퓨터 (Host OS)
├── Docker CLI (명령어 도구)
├── Docker Engine (핵심)
│ ├── Docker API
│ ├── containerd (컨테이너 관리)
│ └── runC (실제 실행기)
└── Linux Kernel
├── Namespaces (격리 기술)
├── Cgroups (자원 제한)
└── Union FS (파일시스템)
Linux 커널 기술의 활용
Docker의 "마법"은 실제로는 Linux 커널의 세 가지 기술을 조합한 것입니다:
1. Namespaces (격리 기술)
Host 시스템에서 실행 중인 프로세스들:
- PID 100: bash
- PID 200: mysql
- PID 300: nginx
컨테이너 A 내부에서 보는 프로세스:
- PID 1: mysql (실제로는 Host의 PID 200)
컨테이너 B 내부에서 보는 프로세스:
- PID 1: nginx (실제로는 Host의 PID 300)
각 컨테이너는 자신만의 독립된 프로세스 공간을 가진 것처럼 보이지만, 실제로는 Host에서 돌아가는 프로세스들입니다.
2. Cgroups (자원 제한)
Host 시스템: CPU 8코어, RAM 16GB
컨테이너 A: CPU 2코어, RAM 1GB로 제한
컨테이너 B: CPU 1코어, RAM 512MB로 제한
3. Union File System (레이어 시스템)
mysql:8.0 이미지 구조:
├── MySQL 설정 레이어
├── MySQL 바이너리 레이어
├── 기본 라이브러리 레이어
└── 베이스 Ubuntu 레이어
컨테이너 실행 시:
├── 쓰기 가능 레이어 (컨테이너별 고유)
└── 위의 모든 레이어들 (읽기 전용, 공유)
세 번째 의문: "가상 공간이라는데, 실제로는 어디에 저장되나요?"
"가상의 공간"이라는 표현 때문에 처음에는 뭔가 허공에 떠있는 것처럼 느껴졌습니다. 하지만 실제로는 모든 것이 물리적 하드디스크에 저장됩니다.
실제 저장 위치
Linux 시스템의 경우:
/var/lib/docker/
├── overlay2/ # 실제 컨테이너 파일들
├── containers/ # 컨테이너 설정
├── images/ # 이미지 정보
├── volumes/ # 볼륨 데이터
└── networks/ # 네트워크 설정
예를 들어, MySQL 컨테이너에서 /tmp/test.txt 파일을 생성하면, 실제로는 Host의 /var/lib/docker/overlay2/abc123.../merged/tmp/test.txt에 저장됩니다.
"가상"의 진짜 의미
Docker의 "가상 공간"이란:
- 존재하지 않는 것: X
- 실제로는 공유하지만 독립된 것처럼 보이게 하는 것: O
Linux 커널의 Namespace 기능이 각 컨테이너에게 "마치 독립된 컴퓨터인 것처럼" 보여주는 것입니다.
네 번째 탐구: "왜 굳이 독립된 것처럼 보이게 하나요?"
이론적으로는 이해가 되었지만, 왜 굳이 "독립된 것처럼" 속이는 건지 궁금했습니다.
격리 없는 현실의 문제들
문제 1: 포트 충돌
첫 번째 앱: ./app1 --port 8080 ✅ 성공
두 번째 앱: ./app2 --port 8080 ❌ Error: Port 8080 already in use
문제 2: 라이브러리 버전 충돌
시스템 Python: 3.8
├── 머신러닝 앱: Python 3.11 필요 ❌
├── 웹 크롤러: Python 3.7 필요 ❌
└── API 서버: Python 3.8 사용 ✅
결과: 하나 업그레이드하면 다른 앱이 깨짐
문제 3: 메모리 독점
정상 상황: 웹서버 1GB + DB 2GB + 캐시 1GB = 4GB ✅
문제 상황: 위 3개 + 버그있는 앱 8GB = 서버 다운 ❌
실제 회사 사례
Case 1: 신입 개발자의 실수
격리 없는 환경:
신입이 메모리 누수 코드 배포 → 서버 전체 다운 → 수억원 손실
격리된 환경 (Docker):
신입이 메모리 누수 코드 배포 → 해당 컨테이너만 재시작 → 문제 해결
결국 "모든 앱이 안전하고 안정적으로 돌아가게 하려고" 독립된 것처럼 보이게 하는 거였습니다.
다섯 번째 발견: "이거 MSA 구조 같은데?"
Docker의 구조를 이해하면서 문득 "이거 마이크로서비스 아키텍처(MSA) 같은데?"라는 생각이 들었습니다.
Docker = 작은 MSA
Docker Host (가상 공간)
├── 컨테이너 A (독립 서버 1)
│ ├── DNS: user-service
│ ├── Port: 8080
│ └── Tech: Java + Spring Boot
├── 컨테이너 B (독립 서버 2)
│ ├── DNS: order-service
│ ├── Port: 8081
│ └── Tech: Python + Flask
└── 컨테이너 C (독립 서버 3)
├── DNS: database
├── Port: 3306
└── Tech: MySQL
정말로 Docker는 하나의 컴퓨터 안에서 여러 개의 독립된 서버를 시뮬레이션하는 "미니 MSA" 환경이었습니다.
여섯 번째 궁금증: "컨테이너끼리는 어떻게 통신하나요?"
MSA처럼 독립된 서버들이라면, 서로 다른 언어/환경에서도 HTTP 통신이 되는 게 신기했습니다.
HTTP 프로토콜의 표준화
서로 다른 언어로 작성된 컨테이너끼리도 HTTP 통신이 가능한 이유는 HTTP가 국제 표준이기 때문입니다.
Java 서비스에서 전송:
POST /api/users HTTP/1.1
Host: python-service:5000
Content-Type: application/json
{"name": "홍길동", "email": "hong@example.com"}
Python 서비스에서 수신:
POST /api/users HTTP/1.1
Host: python-service:5000
Content-Type: application/json
{"name": "홍길동", "email": "hong@example.com"}
모든 언어의 HTTP 라이브러리가 동일한 RFC 표준을 따르므로, 어떤 조합이든 통신이 가능합니다.
Docker 네트워크의 마법
그런데 더 신기한 건 컨테이너들이 어떻게 서로를 찾아가는가였습니다.
일반적인 MSA:
서버 A: http://192.168.1.10:8080/api/users # 실제 IP 필요
서버 B: http://192.168.1.20:8081/api/orders
Docker 네트워크:
컨테이너 A: http://user-service:8080/api/users # 이름으로 접근!
컨테이너 B: http://order-service:8081/api/orders
Docker DNS의 동작 원리
Docker는 내장 DNS 서버를 제공합니다:
1단계: order-service에서 "user-service에 요청 보낼게"
2단계: Docker DNS (127.0.0.11): "user-service = 172.18.0.2야!"
3단계: 실제 HTTP 요청: http://172.18.0.2:8080/api/users
# 컨테이너 내부에서 확인
nslookup user-service
# Server: 127.0.0.11 ← Docker 내장 DNS
# Address: 172.18.0.2 ← 자동 변환된 IP
마지막 질문: "독립적인데 왜 docker-compose.yml이 필요한가요?"
각 컨테이너가 독립적이라면 굳이 설정을 맞춰줄 필요가 없을 것 같았는데, 왜 docker-compose.yml이 필요한지 궁금했습니다.
독립적 ≠ 혼자서 모든 것
아파트에 비유해보겠습니다:
각 세대는 독립적이지만...
❌ 완전히 혼자서는 살 수 없습니다:
- 전기가 필요 (공통 인프라)
- 수도가 필요 (공통 인프라)
- 인터넷이 필요 (공통 인프라)
✅ 아파트 관리사무소가 필요한 이유:
- 모든 세대가 함께 쓸 인프라 관리
- 입주 순서 조율 (전기 먼저, 그 다음 수도...)
- 문제 발생시 통합 관리
Docker도 마찬가지입니다:
각 컨테이너는 독립적이지만...
❌ 완전히 혼자서는 작동 안 됩니다:
- 네트워크가 필요 (서로 통신용)
- 볼륨이 필요 (데이터 저장용)
- 실행 순서가 중요 (DB 먼저, 그 다음 앱)
✅ docker-compose.yml이 필요한 이유:
- 모든 컨테이너가 함께 쓸 인프라 관리
- 실행 순서 조율 (depends_on)
- 설정값들 통합 관리
수동 vs Compose 비교
docker-compose.yml 없이 수동으로:
# 매번 이런 긴 명령어들...
docker network create taskflow-network
docker run -d --name mysql --network taskflow-network -e MYSQL_ROOT_PASSWORD=secret123...
docker run -d --name redis --network taskflow-network redis:7.0
sleep 30 # DB 준비될 때까지 기다리기...
docker run -d --name app --network taskflow-network taskflow:latest
# 새 팀원: "이걸 다 외워야 하나요?"
docker-compose.yml 있으면:
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
app:
image: taskflow:latest
depends_on:
- mysql
docker-compose up -d
# 새 팀원: "이게 끝이에요?"
오케스트라 비유
독립적인 각 악기(컨테이너)들이 아름다운 음악을 연주하려면 지휘자(docker-compose)가 필요합니다:
지휘자 없이 연주: 각자 다른 속도, 다른 키 → 소음
지휘자와 함께 연주: 같은 속도, 같은 키 → 아름다운 음악
Epilogue: Docker 여정을 마치며
처음 "Docker가 뭔가요?"라는 질문에서 시작된 여정이 "Docker = 노트북 안의 미니 MSA"라는 깨달음으로 마무리되었습니다.
핵심 깨달음들
- Docker의 본질: 애플리케이션과 환경을 함께 패키징하는 "이사용 컨테이너"
- 격리의 실체: Linux 커널 기술로 구현된 "선한 속임수"
- 네트워크의 마법: 내장 DNS로 컨테이너 이름을 IP로 자동 변환
- MSA의 시뮬레이션: 하나의 컴퓨터에서 여러 독립 서버 구현
- 협력의 필요성: 독립적이지만 공통 인프라는 함께 관리
실무에서의 가치
전통적 MSA: 서버 5대 + 몇 주 설정 + 수백만원
Docker MSA: 노트북 1대 + 몇 분 설정 + 무료
같은 결과, 다른 방법!
이 여정을 통해 단순히 도구 사용법을 배운 것이 아니라, 현대적인 소프트웨어 아키텍처의 핵심 개념들을 이해할 수 있었습니다. Docker는 단순한 컨테이너 기술으로만 이해되기엔, 유용한 아키텍처를 지니고있었습니다.