안녕하세요, 여러분! 👋 팀 프로젝트를 진행하다 보면 "어? 제 PC에서는 잘 돌아가는데..." 하는 당혹스러운 순간, 한 번쯤 경험해보셨을 겁니다. 개발자마다 다른 OS, 라이브러리 버전, 설정 등으로 인해 발생하는 이러한 '개발 환경 불일치' 문제는 생각보다 큰 생산성 저하를 유발하곤 하죠. 😭
하지만 이런 문제로부터 우리를 구원해 줄 멋진 기술이 있습니다! 바로 컨테이너 기술, 그리고 그 대표주자인 Docker인데요. 이번 5화에서는 Docker가 무엇인지, 왜 개발 환경 관리에 혁명을 가져왔는지 알아보고, Dockerfile과 Docker Compose를 활용하여 효율적인 개발 환경을 구축하는 기본 방법을 쉽고 재미있게 안내해 드리겠습니다. Docker와 함께라면 더 이상 환경 문제로 골머리를 앓지 않고, 오롯이 개발에만 집중할 수 있게 될 거예요! 🚀
📦 컨테이너란 무엇일까요? (가상 머신과의 차이점)
컨테이너를 이해하기 위해 먼저 화물 운송용 컨테이너를 떠올려봅시다. 다양한 크기와 모양의 화물들을 규격화된 컨테이너에 담아 운송하면, 어떤 운송 수단(배, 기차, 트럭)을 이용하든 동일하게 취급하고 옮길 수 있죠? 소프트웨어 세계의 컨테이너도 이와 유사합니다!
- 컨테이너: 애플리케이션과 그 실행에 필요한 모든 종속성(라이브러리, 시스템 도구, 코드, 런타임 등)을 패키징한 격리된 실행 환경입니다.
- 격리성: 각 컨테이너는 호스트 시스템 및 다른 컨테이너로부터 격리되어 독립적으로 실행됩니다. 마치 각자의 방을 가진 것처럼요! 🏠
- 경량성: 기존의 가상 머신(VM)과는 달리, 컨테이너는 운영체제(OS) 커널을 호스트 시스템과 공유합니다. VM처럼 각 컨테이너마다 OS 전체를 설치할 필요가 없어 매우 가볍고 빠릅니다. (VM은 집 전체를 빌리는 것, 컨테이너는 집 안의 방 하나를 빌리는 것에 비유할 수 있어요.)
가상 머신(VM) vs 컨테이너:
특징 | 가상 머신 | 컨테이너 |
OS | 각 VM마다 게스트 OS 전체 포함 | 호스트 OS 커널 공유, OS 핵심 기능만 포함 |
크기 | 큼 (수 GB 이상) | 작음 (수 MB ~ 수백 MB) |
부팅 속도 | 느림 (수 분) | 빠름 (수 초 이내) |
리소스 사용 | 많음 | 적음 |
격리 수준 | 매우 높음 (커널 수준 격리) | 높음 (프로세스 수준 격리) |
주 사용처 | 완전히 다른 OS 환경 필요 시, 하드웨어 가상화 | 애플리케이션 배포 및 격리, 개발 환경 표준화 |
컨테이너는 이러한 특징 덕분에 개발 환경부터 테스트, 배포에 이르기까지 애플리케이션의 라이프사이클 전반에 걸쳐 일관성을 제공합니다.
🤔 개발 환경 관리에 Docker가 필요한 이유
그렇다면 왜 개발자들이 Docker에 열광하고, 개발 환경 관리에 적극적으로 활용할까요?
- 종속성 관리 용이성 ⚙️: 프로젝트에 필요한 모든 라이브러리, 프레임워크, 도구들을 컨테이너 안에 '캡슐화'합니다. 개발자 PC에 특정 버전의 Python, Node.js, Java 등을 일일이 설치하고 관리할 필요가 없어집니다.
- "내 PC에선 되는데..." 문제 해결 ✨: 모든 팀원이 동일한 Docker 이미지를 기반으로 개발 환경을 구성하므로, OS나 로컬 설정 차이로 인한 문제를 원천적으로 방지할 수 있습니다. "It works on my machine"이라는 말은 이제 그만!
- 새 프로젝트/팀 합류 시 빠른 환경 설정 ⏱️: 새로운 프로젝트에 참여하거나 신규 팀원이 합류했을 때, 복잡한 설치 과정 없이 Docker 명령어 몇 줄로 즉시 개발 환경을 갖출 수 있습니다. 온보딩 시간이 획기적으로 단축됩니다.
- 운영 환경과의 일관성 유지 ↔️: 개발 환경에서 사용한 Docker 이미지를 그대로 테스트, 스테이징, 운영 환경에서도 사용할 수 있습니다. 각 환경 간의 차이를 최소화하여 배포 시 발생할 수 있는 예기치 않은 문제를 줄여줍니다.
- 다양한 기술 스택 실험 용이 🧪: 새로운 언어나 프레임워크, 데이터베이스를 사용해보고 싶을 때, 로컬 시스템을 더럽히지 않고 Docker 컨테이너 안에서 간편하게 테스트해볼 수 있습니다.
🐳 Docker 기본 개념: 이미지(Image)와 컨테이너(Container)
Docker를 이해하기 위한 가장 핵심적인 두 가지 개념은 바로 이미지와 컨테이너입니다.
- 이미지 (Image) 📜:
- 애플리케이션과 그 실행 환경을 담고 있는 읽기 전용 템플릿입니다. 컨테이너를 생성하기 위한 설계도, 혹은 실행 파일을 만들기 전의 소스 코드와 같다고 생각할 수 있습니다.
- 이미지는 여러 개의 계층(Layer)으로 구성되며, Docker Hub와 같은 레지스트리에서 공개 이미지를 가져오거나(pull), 직접 만들어(build) 사용할 수 있습니다.
- 예를 들어, "Ubuntu OS 위에 Python 3.9가 설치되고, 내 Flask 애플리케이션 코드가 복사된" 상태를 하나의 이미지로 만들 수 있습니다.
- 비유: 붕어빵 틀, 요리 레시피, 프로그램 설치 CD
- 컨테이너 (Container) İz:
- 이미지를 기반으로 실제 메모리에 로드되어 실행되는 인스턴스입니다. 하나의 이미지로부터 여러 개의 독립적인 컨테이너를 생성할 수 있습니다.
- 컨테이너는 격리된 공간에서 자신만의 파일 시스템, 네트워크, 프로세스를 가집니다.
- 애플리케이션이 실제로 동작하는 살아있는 상태라고 볼 수 있습니다.
- 비유: 붕어빵 틀로 찍어낸 붕어빵, 레시피로 만든 요리, CD로 설치해서 실행 중인 프로그램
간단히 말해, 이미지는 "설계도"이고, 컨테이너는 그 설계도로 "지은 집"입니다! 🏠
📝 Dockerfile 작성법
Dockerfile은 Docker 이미지를 만들기 위한 텍스트 파일 기반의 명세서입니다. 이 파일 안에 순차적으로 명령어를 작성하면, Docker가 이를 읽어들여 자동으로 이미지를 빌드해줍니다. 마치 요리 레시피처럼, 어떤 재료(베이스 이미지)를 사용하고, 어떤 과정을 거쳐(명령어 실행) 최종 요리(이미지)를 완성할지 정의하는 것이죠.
주요 Dockerfile 명령어는 다음과 같습니다.
- FROM <이미지>:<태그>: 베이스 이미지를 지정합니다. (예: FROM ubuntu:22.04, FROM python:3.9-slim) 모든 Dockerfile은 FROM으로 시작해야 합니다.
- WORKDIR /<경로>: 이후 RUN, CMD, ENTRYPOINT, COPY, ADD 명령어가 실행될 작업 디렉토리를 설정합니다.
- COPY <호스트_경로> <이미지_내_경로>: 호스트 머신의 파일이나 디렉토리를 이미지 안으로 복사합니다. (예: COPY . /app)
- ADD <호스트_경로_또는_URL> <이미지_내_경로>: COPY와 유사하지만, URL에서 파일을 다운로드하거나 압축 파일을 자동으로 해제하는 기능도 있습니다.
- RUN <명령어>: 이미지 빌드 과정에서 실행할 명령어를 정의합니다. 주로 패키지 설치, 디렉토리 생성 등에 사용됩니다. (예: RUN apt-get update && apt-get install -y curl) 각 RUN 명령어는 새로운 이미지 레이어를 생성합니다.
- EXPOSE <포트>: 컨테이너가 실행될 때 외부에 노출할 포트를 명시합니다. (실제 포트 매핑은 docker run 시 -p 옵션으로 지정)
- CMD ["실행파일", "파라미터1", "파라미터2"] 또는 CMD 명령어 파라미터1 파라미터2 (쉘 형식): 컨테이너가 시작될 때 기본적으로 실행될 명령어를 지정합니다. Dockerfile 내에서 한 번만 사용할 수 있으며, docker run 시 다른 명령어를 전달하면 덮어써집니다.
- ENTRYPOINT ["실행파일", "파라미터1", "파라미터2"]: CMD와 유사하지만, 컨테이너를 실행 파일처럼 사용할 때 주로 쓰이며, docker run 시 전달된 인자들을 ENTRYPOINT 명령어의 인자로 추가합니다.
🎶 Docker Compose: 여러 컨테이너를 한 번에 관리하기
애플리케이션은 보통 웹 서버, 데이터베이스, 캐시 서버 등 여러 서비스(컨테이너)로 구성됩니다. 이들을 각각 docker run 명령어로 실행하고 관리하는 것은 번거롭겠죠? 이때 Docker Compose가 등장합니다!
- 왜 필요한가? 🤔
- 여러 컨테이너로 구성된 애플리케이션(예: 웹 앱 + DB + Redis)을 정의하고 실행, 중지하는 작업을 간편하게 관리할 수 있습니다.
- 각 컨테이너 간의 네트워크 연결, 볼륨 공유, 의존성 설정 등을 하나의 YAML 파일로 관리합니다.
- docker-compose.yml 파일 기본 구조: docker-compose.yml (또는 compose.yaml) 파일은 YAML 형식으로 작성되며, 애플리케이션을 구성하는 서비스, 네트워크, 볼륨 등을 정의합니다.
-
version: '3.8' # 또는 최신 버전 명시 (선택적) services: web: # 서비스 이름 (임의 지정 가능) build: . # Dockerfile이 있는 경로 (현재 디렉토리) ports: - "5000:5000" # <호스트 포트>:<컨테이너 포트> volumes: - .:/app # <호스트 경로>:<컨테이너 경로> (코드 변경 실시간 반영) depends_on: # 의존성 설정 - db environment: - DATABASE_URL=postgresql://user:password@db:5432/mydb db: # 또 다른 서비스 (예: 데이터베이스) image: postgres:15 # Docker Hub의 공식 PostgreSQL 이미지 사용 volumes: - postgres_data:/var/lib/postgresql/data # 데이터 영속성을 위한 볼륨 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=password - POSTGRES_DB=mydb ports: # 개발 시 DB 직접 접근을 위해 (선택적) - "5432:5432" volumes: # 명명된 볼륨 정의 postgres_data:
- 자주 사용하는 옵션:
- services: 애플리케이션을 구성하는 각 컨테이너(서비스)를 정의합니다.
- build: Dockerfile의 경로를 지정하여 이미지를 빌드합니다. 컨텍스트 경로(context)와 Dockerfile 이름(dockerfile)을 명시할 수도 있습니다.
- image: 사용할 Docker 이미지 이름을 지정합니다. (예: nginx:latest, redis)
- ports: 호스트와 컨테이너 간의 포트를 매핑합니다. ["<호스트_포트>:<컨테이너_포트>"] 형식입니다.
- volumes: 호스트의 디렉터리나 명명된 볼륨을 컨테이너의 특정 경로에 마운트하여 데이터를 영속화하거나 파일을 공유합니다.
- environment: 컨테이너 내에서 사용할 환경 변수를 설정합니다.
- depends_on: 서비스 간의 의존성을 정의하여 특정 서비스가 시작된 후에 다른 서비스가 시작되도록 순서를 제어합니다. (완전한 준비 상태를 보장하진 않으므로 애플리케이션 레벨의 재시도 로직이 필요할 수 있습니다.)
- networks: 컨테이너들이 통신할 사용자 정의 네트워크를 설정합니다.
🚀 예시/활용법: Docker로 실제 개발 환경 구축하기
1. 간단한 Python Flask 웹 애플리케이션 Dockerfile 예시
다음은 간단한 "Hello, Docker!"를 출력하는 Python Flask 웹 애플리케이션을 위한 Dockerfile입니다.
app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_docker():
return 'Hello, Docker! This is Flask. 🐳'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
requirements.txt:
Flask==2.3.3
Dockerfile:
# 1. 베이스 이미지 선택 (Python 3.9 슬림 버전)
FROM python:3.9-slim
# 2. 작업 디렉토리 설정
WORKDIR /app
# 3. 필요한 파일 복사 (requirements.txt 먼저 복사하여 레이어 캐싱 활용)
COPY requirements.txt requirements.txt
# 4. 종속성 설치
RUN pip install --no-cache-dir -r requirements.txt
# 5. 나머지 애플리케이션 코드 복사
COPY . .
# 6. 컨테이너 실행 시 Flask 애플리케이션 실행
CMD ["python", "app.py"]
빌드 및 실행 명령어:
# 1. Docker 이미지 빌드 (현재 디렉토리에 Dockerfile이 있다고 가정)
# -t 옵션으로 이미지 이름과 태그(my-flask-app:latest)를 지정합니다.
$ docker build -t my-flask-app .
# 2. 빌드된 이미지로 컨테이너 실행
# -p 옵션으로 호스트의 5000번 포트와 컨테이너의 5000번 포트를 연결합니다.
# --name 옵션으로 컨테이너 이름을 지정합니다 (선택 사항).
# -d 옵션으로 백그라운드에서 실행합니다 (선택 사항).
$ docker run -d -p 5000:5000 --name flask_hello my-flask-app
이제 웹 브라우저에서 http://localhost:5000으로 접속하면 "Hello, Docker! This is Flask. 🐳" 메시지를 볼 수 있습니다.
2. 웹 애플리케이션과 PostgreSQL 데이터베이스를 함께 구성하는 docker-compose.yml 예시
위 Flask 애플리케이션이 PostgreSQL 데이터베이스를 사용한다고 가정하고, docker-compose.yml 파일을 작성해봅시다.
docker-compose.yml:
version: '3.8'
services:
web:
build: . # 현재 디렉토리의 Dockerfile 사용 (위 Flask 앱)
container_name: my_flask_app_container
ports:
- "5000:5000"
volumes:
- .:/app # 개발 중 코드 변경 시 실시간 반영
depends_on:
- db # db 서비스가 시작된 후 web 서비스 시작
environment:
# 실제 애플리케이션에서 이 환경 변수를 사용하도록 코드를 수정해야 합니다.
- DATABASE_URL=postgresql://myuser:mypassword@db:5432/myappdb
- FLASK_ENV=development # Flask 개발 모드 활성화
db:
image: postgres:15-alpine # 가벼운 Alpine 버전 PostgreSQL 이미지
container_name: my_postgres_db
ports: # 개발 시 호스트에서 DB에 직접 연결할 필요가 있을 때
- "5433:5432" # 호스트 5433 포트 -> 컨테이너 5432 포트
volumes:
- postgres_data:/var/lib/postgresql/data # 데이터 영속성을 위한 명명된 볼륨
environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypassword
- POSTGRES_DB=myappdb
volumes:
postgres_data: # 명명된 볼륨 정의 (컨테이너가 삭제되어도 데이터 유지)
(app.py에서 DATABASE_URL 환경 변수를 사용하도록 코드를 수정해야 합니다.)
실행 명령어:
# 1. docker-compose.yml 파일이 있는 디렉토리에서 실행
# 정의된 모든 서비스를 빌드하고 실행합니다. (-d는 백그라운드 실행)
$ docker compose up -d
# 2. 실행 중인 서비스 확인
$ docker compose ps
# 3. 서비스 중지 및 컨테이너, 네트워크 등 삭제
$ docker compose down
# 4. (선택) 데이터 볼륨까지 삭제하려면:
$ docker compose down -v
이제 docker compose up -d 명령 한 번으로 Flask 웹 서버와 PostgreSQL 데이터베이스가 함께 실행되는 환경을 손쉽게 구성할 수 있습니다!
🎉 마무리 및 다음 화 예고
Docker를 활용하면 "내 PC에선 되는데..."라는 오랜 숙제를 해결하고, 팀 전체의 개발 환경을 표준화하여 생산성을 극대화할 수 있습니다. 처음에는 Dockerfile이나 docker-compose.yml 작성이 조금 낯설 수 있지만, 몇 번만 직접 구성해보면 그 편리함에 매료될 거예요! 오늘 배운 내용을 바탕으로 여러분의 프로젝트에도 Docker를 도입하여 신세계를 경험해보시길 바랍니다. 🥳
다음 개발자의 생산성을 높이는 도구/팁 시리즈 6화에서는 "API 테스트 도구 활용 (Postman/Insomnia 등)" 에 대해 알아봅니다. 직접 개발한 API는 물론, 외부 API를 효율적으로 테스트하고 관리하는 방법을 기대해주세요! 📬