2026년 6월 20일

마스터하기: 도커(Docker)와 컨테이너 - 현대 소프트웨어 배포의 혁명

10
마스터하기: 도커(Docker)와 컨테이너 - 현대 소프트웨어 배포의 혁명

마스터하기: 도커(Docker)와 컨테이너 - 현대 소프트웨어 배포의 혁명

마스터하기: 도커(Docker)와 컨테이너 - 현대 소프트웨어 배포의 혁명

안녕하세요! 10년차 소프트웨어 엔지니어이자 기술 교육자로서, 오늘 여러분과 함께 현대 소프트웨어 개발의 필수 요소이자 가장 혁신적인 기술 중 하나인 '도커(Docker)와 컨테이너'에 대해 깊이 있게 알아보는 시간을 가지려고 합니다. "내 컴퓨터에서는 잘 되는데, 서버에 올리니 안 돼요!"라는 말, 개발자라면 한 번쯤은 들어봤을 겁니다. 이러한 고질적인 문제를 해결하고, 개발 및 배포 환경을 혁신한 주인공이 바로 도커와 컨테이너입니다. 2026년인 지금, 이 기술 없이는 효율적인 개발과 안정적인 서비스 운영을 상상하기 어렵습니다. 초중급 개발자 여러분이 도커를 단순히 명령어 몇 개를 외우는 것을 넘어, 그 핵심 원리와 실질적인 활용법을 이해하고 능숙하게 다룰 수 있도록 돕는 것이 이 글의 목표입니다.

1. 개념 소개

1. 개념 소개

정의: 도커(Docker)와 컨테이너(Container)

도커는 컨테이너 기반의 가상화 기술을 사용하여 애플리케이션을 쉽고 빠르게 빌드, 배포, 실행할 수 있도록 돕는 오픈소스 플랫폼입니다. 여기서 핵심은 '컨테이너'입니다. 컨테이너는 애플리케이션과 그 애플리케이션을 실행하는 데 필요한 모든 종속성(라이브러리, 설정 파일 등)을 함께 묶어 격리된 공간에서 실행할 수 있도록 해주는 패키지입니다. 마치 특정 물건을 담는 규격화된 상자처럼, 어떤 환경에서든 동일하게 동작하도록 보장하는 역할을 합니다.

가상 머신(Virtual Machine, VM)과의 차이점: 도커 컨테이너를 이해할 때 가상 머신(VM)과 비교하면 훨씬 명확해집니다.

| 특징 | 가상 머신 (VM) | 도커 컨테이너 | | :------------ | :------------------------------------------- | :------------------------------------------------ | | 격리 단위 | 하드웨어 전체 가상화 | 운영체제 커널 공유, 프로세스 수준 격리 | | 구성 요소 | 호스트 OS, 하이퍼바이저, 게스트 OS, 애플리케이션 | 호스트 OS, 도커 엔진, 애플리케이션 및 종속성 | | 시작 시간 | 수 분 (게스트 OS 부팅 필요) | 수 초 (OS 부팅 불필요) | | 자원 사용 | 무거움 (각 VM마다 OS 자원 소모) | 가벼움 (OS 자원 공유, 필요한 만큼만 사용) | | 이식성 | 이미지 크기가 커서 이동 및 배포가 번거로움 | 이미지 크기가 작고, 어디서든 동일하게 실행 가능 |

VM은 별도의 운영체제를 통째로 가상화하는 반면, 컨테이너는 호스트 운영체제의 커널을 공유하면서 프로세스 수준에서 격리됩니다. 이 덕분에 컨테이너는 VM보다 훨씬 가볍고 빠르게 동작하며, 리소스 효율이 뛰어납니다.

탄생 배경: "내 컴퓨터에서는 되는데, 서버에서는 안 돼요!"

도커가 등장하기 전에는 개발자들이 다음과 같은 문제에 자주 직면했습니다:

  • 환경 불일치: 개발 환경, 테스트 환경, 운영 환경이 각기 달라서 애플리케이션이 특정 환경에서만 작동하거나 예기치 않은 오류를 발생시키는 경우가 많았습니다. 개발자의 PC에서는 잘 돌아가던 코드가 서버에 배포하면 라이브러리 버전 문제, 설정 파일 누락 등으로 인해 작동하지 않는 일이 비일비재했습니다.
  • 복잡한 배포 과정: 애플리케이션을 배포하려면 필요한 모든 종속성을 수동으로 설치하고 설정해야 했습니다. 이는 시간이 많이 소요되고 오류 발생 가능성이 높았습니다.
  • 자원 낭비: VM은 강력한 격리를 제공하지만, 각 VM마다 별도의 OS를 포함하기 때문에 많은 디스크 공간과 메모리, CPU 자원을 소모했습니다.

이러한 문제들은 개발 생산성을 저해하고 배포 과정을 어렵게 만들었습니다. 도커는 이러한 문제들을 해결하기 위해 표준화된 방식으로 애플리케이션을 패키징하고 격리하여 어떤 환경에서든 일관성 있게 실행될 수 있도록 하는 솔루션으로 탄생했습니다.

왜 중요한가: 현대 개발의 필수 요소

도커와 컨테이너 기술은 다음과 같은 이유로 현대 소프트웨어 개발 및 운영에 있어 핵심적인 기술로 자리매김했습니다.

  • 환경 일관성: 개발, 테스트, 운영 환경에 관계없이 동일한 컨테이너 이미지를 사용하여 "한 번 빌드하면 어디서든 실행(Build once, Run anywhere)"할 수 있는 환경을 제공합니다. 이는 환경 불일치로 인한 버그를 최소화하고 개발자의 디버깅 시간을 줄여줍니다.
  • 빠르고 쉬운 배포: 애플리케이션과 모든 종속성이 컨테이너 이미지 하나로 패키징되므로, 이 이미지를 서버에 전달하고 실행하기만 하면 됩니다. 배포 과정이 단순해지고 자동화하기 쉬워집니다.
  • 효율적인 자원 사용: VM에 비해 훨씬 가볍고 빠르게 시작되며, 호스트 OS의 커널을 공유하여 자원 오버헤드를 줄입니다. 이는 서버 비용 절감과 더 많은 애플리케이션을 하나의 서버에서 실행할 수 있게 해줍니다.
  • 마이크로서비스 아키텍처 지원: 각 마이크로서비스를 독립적인 컨테이너로 패키징하여 배포하고 관리할 수 있게 함으로써, 마이크로서비스 아키텍처의 도입과 운영을 크게 용이하게 합니다. 각 서비스의 독립적인 확장 및 배포가 가능해집니다.
  • 이식성 및 확장성: 컨테이너는 어떤 클라우드 제공업체나 온프레미스 환경에서도 동일하게 작동하므로, 특정 환경에 종속되지 않고 자유롭게 이동할 수 있습니다. 또한, 필요에 따라 컨테이너 인스턴스를 쉽게 늘리거나 줄여 애플리케이션의 확장성을 확보할 수 있습니다.

2. 핵심 원리 설명

2. 핵심 원리 설명

도커 컨테이너의 작동 원리를 이해하는 것은 도커를 효과적으로 활용하는 데 매우 중요합니다. 주요 개념과 비유를 통해 알아보겠습니다.

컨테이너의 작동 원리

  1. 이미지(Image)와 컨테이너(Container):

    • 이미지는 애플리케이션을 실행하는 데 필요한 모든 것(코드, 런타임, 시스템 도구, 라이브러리, 설정 등)을 포함하는 읽기 전용의 템플릿입니다. 마치 붕어빵 틀이나 소프트웨어 설치 파일과 같습니다.
    • 컨테이너는 이 이미지를 기반으로 실행되는 애플리케이션의 격리된 인스턴스입니다. 붕어빵 틀로 만들어진 실제 붕어빵, 혹은 설치 파일을 실행해서 설치된 프로그램과 같습니다. 이미지는 여러 컨테이너를 생성하는 데 사용될 수 있습니다.
  2. 레이어(Layer) 시스템: 도커 이미지는 여러 개의 읽기 전용 레이어들로 구성됩니다. 각 레이어는 Dockerfile의 명령 하나하나에 해당하며, 이전 레이어 위에 변경 사항을 추가합니다. 예를 들어, FROM ubuntu:22.04는 하나의 레이어, RUN apt-get update는 또 다른 레이어, COPY . /app도 레이어입니다. 이 레이어 시스템의 장점은 다음과 같습니다:

    • 효율적인 저장 공간: 여러 이미지가 동일한 기본 레이어를 공유할 수 있어 디스크 공간을 절약합니다.
    • 빠른 빌드: 변경된 레이어만 다시 빌드하고 캐시된 이전 레이어를 재사용하여 이미지 빌드 시간을 단축합니다.
    • 버전 관리: 각 레이어가 변경 이력을 나타내므로 이미지의 변경 사항을 추적하기 쉽습니다.
  3. 네임스페이스(Namespaces)를 통한 격리: 컨테이너는 호스트 OS의 커널을 공유하지만, 네임스페이스라는 리눅스 커널 기능을 사용하여 각 컨테이너가 마치 독립된 시스템처럼 보이도록 격리합니다.

    • PID 네임스페이스: 각 컨테이너는 자신만의 프로세스 ID (PID) 공간을 가집니다. 컨테이너 내부의 PID 1은 해당 컨테이너의 주 프로세스입니다.
    • Mount 네임스페이스: 각 컨테이너는 자신만의 파일 시스템 마운트 포인트를 가집니다.
    • Network 네임스페이스: 각 컨테이너는 자신만의 네트워크 인터페이스, IP 주소, 라우팅 테이블 등을 가집니다.
    • User 네임스페이스: 각 컨테이너는 자신만의 사용자 및 그룹 ID 매핑을 가질 수 있습니다. 이러한 네임스페이스 덕분에 컨테이너 내의 프로세스는 호스트나 다른 컨테이너의 프로세스, 파일 시스템, 네트워크에 직접 접근할 수 없으며, 각자 독립적으로 동작합니다.
  4. 컨트롤 그룹(Control Groups, Cgroups)을 통한 자원 제한: 컨트롤 그룹은 리눅스 커널 기능으로, 특정 프로세스 그룹에 할당될 수 있는 CPU, 메모리, 네트워크 대역폭, 디스크 I/O 등의 자원을 제한하거나 모니터링할 수 있게 해줍니다. 도커는 Cgroups를 사용하여 각 컨테이너가 사용할 수 있는 자원의 양을 제한합니다. 예를 들어, 특정 컨테이너가 호스트의 모든 CPU나 메모리를 독점하는 것을 방지하여, 다른 컨테이너나 호스트 시스템의 안정적인 작동을 보장합니다.

  5. 도커 엔진: 도커 엔진은 컨테이너를 빌드하고 실행하며 관리하는 핵심 소프트웨어입니다.

    • 도커 데몬 (dockerd): 백그라운드에서 실행되며 이미지, 컨테이너, 네트워크, 볼륨 등을 관리하는 도커의 핵심 서버 프로세스입니다.
    • 도커 CLI (Client): 사용자가 도커 데몬과 상호작용하기 위한 명령줄 인터페이스입니다 (docker build, docker run 등).
    • REST API: 도커 데몬이 제공하는 API로, 다른 프로그램이나 스크립트가 도커 기능을 활용할 수 있도록 합니다.

비유: 배송 컨테이너와 레고 블록

  • 배송 컨테이너: 전 세계적으로 사용되는 표준화된 배송 컨테이너를 떠올려 보세요. 이 컨테이너는 내용물이 무엇이든 상관없이 동일한 규격으로 만들어져 배, 기차, 트럭 등 어떤 운송 수단에도 쉽게 실어 나를 수 있습니다. 도커 컨테이너도 마찬가지입니다. 어떤 애플리케이션이든 컨테이너라는 표준화된 "상자"에 담기면, 어떤 서버 환경(개발, 테스트, 운영, 클라우드 등)에서도 동일하게 동작하고 쉽게 이동시킬 수 있습니다.
  • 레고 블록: 도커 이미지를 레고 블록으로 비유할 수 있습니다. 각 레고 블록은 특정 기능(예: 운영체제, 웹 서버, 데이터베이스)을 하는 미리 만들어진 부품입니다. 이 블록들을 조립하여 원하는 구조(애플리케이션)를 만들 수 있습니다. 레고 블록처럼 도커 이미지도 여러 레이어로 구성되어 있고, 필요한 기능만 추가하여 효율적으로 재사용하고 조립할 수 있습니다.

다이어그램 (개념 설명)

[ 가상 머신 (VM) 아키텍처 ]

+----------------------------------------------------------------+
| Host Operating System (Linux, Windows, macOS)                  |
| +------------------------------------------------------------+ |
| | Hypervisor (VMware, VirtualBox, Hyper-V)                   | |
| | +---------------------+  +---------------------+         | |
| | | Guest OS (Linux)    |  | Guest OS (Windows)  |         | |
| | | +-----------------+ |  | +-----------------+ |         | |
| | | | Application A   | |  | | Application B   | |         | |
| | | | Libraries       | |  | | Libraries       | |         | |
| | | +-----------------+ |  | +-----------------+ |         | |
| | +---------------------+  +---------------------+         | |
| +------------------------------------------------------------+ |
+----------------------------------------------------------------+

VM은 각 애플리케이션마다 독립적인 게스트 OS를 포함하여 하이퍼바이저 위에서 실행됩니다. 이는 강력한 격리를 제공하지만, 각 VM이 상당한 시스템 자원을 소모합니다.

[ 도커 컨테이너 아키텍처 ]

+----------------------------------------------------------------+
| Host Operating System (Linux, Windows, macOS)                  |
| +------------------------------------------------------------+ |
| | Docker Engine (Daemon + CLI + REST API)                    | |
| | +---------------------+  +---------------------+         | |
| | | Container 1         |  | Container 2         |         | |
| | | +-----------------+ |  | +-----------------+ |         | |
| | | | Application A   | |  | | Application B   | |         | |
| | | | Libraries       | |  | | Libraries       | |         | |
| | | +-----------------+ |  | +-----------------+ |         | |
| | +---------------------+  +---------------------+         | |
| +------------------------------------------------------------+ |
+----------------------------------------------------------------+

도커 컨테이너는 호스트 OS의 커널을 공유하며 도커 엔진 위에서 실행됩니다. 각 컨테이너는 네임스페이스와 컨트롤 그룹을 통해 격리되지만, 게스트 OS를 포함하지 않으므로 훨씬 가볍고 효율적입니다.

3. 코드 예제 2개

여기서는 Python 웹 애플리케이션을 예시로 들어 도커를 사용하는 방법을 보여드리겠습니다.

예제 1: 간단한 Python 웹 애플리케이션 컨테이너화

간단한 Flask 웹 애플리케이션을 도커 컨테이너로 만드는 과정을 보여줍니다.

1. 애플리케이션 파일 준비: app.py 파일을 생성합니다.

# app.py
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    # 환경 변수에서 이름 가져오기, 없으면 'World'
    name = os.environ.get('APP_NAME', 'World')
    return f"Hello, {name} from a Docker Container!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

requirements.txt 파일을 생성하여 필요한 파이썬 패키지를 명시합니다.

# requirements.txt
Flask==2.3.2

2. Dockerfile 작성: Dockerfile은 도커 이미지를 빌드하는 방법을 정의하는 스크립트입니다.

# Dockerfile

# 1. 어떤 기본 이미지를 사용할지 명시합니다.
#    Python 3.9 버전을 기반으로 하는 경량화된 Alpine 리눅스 이미지를 사용합니다.
FROM python:3.9-alpine

# 2. 컨테이너 내부의 작업 디렉토리를 설정합니다.
#    이후 모든 명령어는 이 디렉토리에서 실행됩니다.
WORKDIR /app

# 3. 호스트의 requirements.txt 파일을 컨테이너의 /app 디렉토리로 복사합니다.
COPY requirements.txt .

# 4. requirements.txt에 명시된 파이썬 패키지들을 설치합니다.
#    --no-cache-dir: 캐시 디렉토리를 사용하지 않아 이미지 크기를 줄입니다.
#    -r: requirements.txt 파일에서 패키지 목록을 읽어옵니다.
RUN pip install --no-cache-dir -r requirements.txt

# 5. 현재 디렉토리(app.py)의 모든 파일을 컨테이너의 /app 디렉토리로 복사합니다.
COPY . .

# 6. 컨테이너가 5000번 포트를 외부에 노출할 것임을 도커에게 알립니다.
#    이는 문서화 목적이며, 실제로 외부에서 접근하려면 `docker run -p` 옵션이 필요합니다.
EXPOSE 5000

# 7. 컨테이너가 시작될 때 실행될 기본 명령어를 정의합니다.
#    이 컨테이너는 Flask 앱을 실행합니다.
#    [ "python", "app.py" ]는 CMD ["executable", "param1", "param2"] 형식입니다.
CMD [ "python", "app.py" ]

3. 도커 이미지 빌드: 터미널에서 app.py, requirements.txt, Dockerfile이 있는 디렉토리로 이동하여 다음 명령어를 실행합니다.

docker build -t my-flask-app:1.0 .
# -t (tag): 이미지 이름과 태그를 지정합니다 (예: my-flask-app:1.0).
# . (dot): Dockerfile이 현재 디렉토리에 있음을 나타냅니다.

빌드가 완료되면 docker images 명령어로 생성된 이미지를 확인할 수 있습니다.

4. 도커 컨테이너 실행:

docker run -p 5000:5000 --name flask-web-server -e APP_NAME="Docker User" my-flask-app:1.0
# -p 5000:5000: 호스트의 5000번 포트를 컨테이너의 5000번 포트와 연결합니다.
#               (호스트 포트:컨테이너 포트)
# --name: 컨테이너에 이름을 부여합니다 (예: flask-web-server).
# -e APP_NAME="Docker User": 컨테이너 내부의 환경 변수 APP_NAME을 설정합니다.
# my-flask-app:1.0: 실행할 이미지의 이름과 태그입니다.

브라우저에서 http://localhost:5000에 접속하면 "Hello, Docker User from a Docker Container!" 메시지를 볼 수 있습니다.

예제 2: 데이터베이스와 함께 다중 컨테이너 애플리케이션 (Docker Compose)

이번에는 웹 애플리케이션과 Redis 데이터베이스를 함께 사용하는 시나리오를 Docker Compose로 구성해 보겠습니다. Docker Compose는 여러 개의 컨테이너를 한 번에 정의하고 실행할 수 있게 해주는 도구입니다.

1. 애플리케이션 파일 준비: app.py 파일을 수정하여 Redis에 접속하고 데이터를 저장/조회하도록 합니다.

# app.py
from flask import Flask, request, jsonify
import os
import redis

app = Flask(__name__)

# 환경 변수에서 Redis 호스트와 포트 가져오기
redis_host = os.environ.get('REDIS_HOST', 'localhost')
redis_port = int(os.environ.get('REDIS_PORT', 6379))

# Redis 클라이언트 초기화
try:
    r = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)
    r.ping() # Redis 서버 연결 확인
    print(f"Connected to Redis at {redis_host}:{redis_port}")
except redis.exceptions.ConnectionError as e:
    print(f"Could not connect to Redis: {e}")
    r = None # 연결 실패 시 r을 None으로 설정

@app.route('/')
def hello():
    name = os.environ.get('APP_NAME', 'World')
    return f"Hello, {name} from a Docker Compose App!"

@app.route('/hit_count', methods=['GET', 'POST'])
def hit_count():
    if r is None:
        return jsonify({"error": "Redis connection failed"}), 500

    if request.method == 'POST':
        r.incr('hits') # 'hits' 키의 값 1 증가
        return jsonify({"message": "Hit count incremented!"})
    else:
        count = r.get('hits') # 'hits' 키의 값 조회
        return jsonify({"hits": int(count) if count else 0})

if __name__ == '__main__':
    app.run(host='0.0.0