🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Docker로 모델 컨테이너화 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 30. · 21 Views

Docker로 모델 컨테이너화 완벽 가이드

머신러닝 모델을 Docker 컨테이너로 패키징하여 어디서든 동일하게 실행하는 방법을 알아봅니다. 개발 환경과 운영 환경의 불일치 문제를 해결하고, 모델 배포를 간소화하는 핵심 기술을 다룹니다.


목차

  1. Docker_기초와_ML_모델_배포의_필요성
  2. requirements.txt_작성과_의존성_관리
  3. Flask_API로_모델_서빙하기
  4. Gunicorn으로_프로덕션_서버_구성하기
  5. Docker_이미지_빌드와_실행
  6. 멀티스테이지_빌드로_이미지_최적화
  7. 환경변수로_설정_분리하기
  8. 헬스체크로_컨테이너_상태_모니터링
  9. Docker_Compose로_다중_컨테이너_관리
  10. 컨테이너_레지스트리에_이미지_배포하기

1. Docker 기초와 ML 모델 배포의 필요성

김개발 씨는 열심히 만든 머신러닝 모델을 팀장에게 시연하려고 했습니다. 그런데 로컬에서 잘 돌아가던 모델이 팀장 컴퓨터에서는 에러를 뿜어냈습니다.

"제 컴퓨터에서는 잘 됐는데요..." 개발자라면 누구나 한 번쯤 해봤을 그 말이 튀어나왔습니다.

Docker는 애플리케이션을 컨테이너라는 독립된 환경에 담아 어디서든 동일하게 실행할 수 있게 해주는 도구입니다. 마치 이사할 때 모든 짐을 컨테이너 박스에 담아 운반하면 새 집에서도 그대로 사용할 수 있는 것과 같습니다.

ML 모델을 Docker로 패키징하면 개발 환경과 운영 환경의 차이로 인한 문제를 완전히 해결할 수 있습니다.

다음 코드를 살펴봅시다.

# Dockerfile - ML 모델을 담을 컨테이너 설계도
FROM python:3.9-slim

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 파일 복사 및 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 모델 파일과 소스 코드 복사
COPY model/ ./model/
COPY app.py .

# 컨테이너 실행 시 수행할 명령
CMD ["python", "app.py"]

김개발 씨는 입사 6개월 차 머신러닝 엔지니어입니다. 지난 한 달간 고객 이탈 예측 모델을 열심히 개발했습니다.

정확도도 높고, 추론 속도도 빠릅니다. 드디어 운영팀에 모델을 전달할 시간입니다.

그런데 문제가 생겼습니다. 운영팀의 서버에서 모델이 실행되지 않는 것입니다.

Python 버전이 달랐고, 필요한 라이브러리도 없었습니다. 김개발 씨가 사용한 TensorFlow 버전과 서버에 설치된 버전도 맞지 않았습니다.

선배 개발자 박시니어 씨가 다가와 말했습니다. "이런 문제를 해결하려면 Docker를 써야 해요.

모델과 모든 환경을 함께 패키징하면 어디서든 똑같이 동작합니다." 그렇다면 Docker란 정확히 무엇일까요? 쉽게 비유하자면, Docker는 마치 완벽하게 갖춰진 이동식 주방과 같습니다.

셰프가 어떤 장소에서 요리를 하든, 이 이동식 주방만 있으면 똑같은 맛의 음식을 만들 수 있습니다. 조리 도구, 양념, 레시피가 모두 함께 들어있기 때문입니다.

Docker가 없던 시절에는 어땠을까요? 개발자들은 운영 서버에 필요한 라이브러리를 하나씩 설치해야 했습니다.

버전 충돌이 일어나면 밤새 삽질을 해야 했습니다. 더 큰 문제는 개발 환경과 운영 환경이 조금만 달라도 예상치 못한 버그가 발생한다는 것이었습니다.

바로 이런 문제를 해결하기 위해 Docker가 등장했습니다. Docker를 사용하면 환경 자체를 코드로 정의할 수 있습니다.

Dockerfile이라는 설계도에 필요한 모든 것을 명시하면, 어떤 서버에서든 동일한 환경을 재현할 수 있습니다. 위의 Dockerfile을 살펴보겠습니다.

먼저 FROM 명령어로 기반이 될 Python 이미지를 지정합니다. 그 다음 WORKDIR로 작업 디렉토리를 설정하고, COPY와 RUN으로 필요한 파일을 복사하고 라이브러리를 설치합니다.

마지막으로 CMD로 컨테이너가 시작될 때 실행할 명령을 지정합니다. 실제 현업에서는 이 기술이 필수입니다.

예를 들어 하루에도 수십 번 모델을 업데이트하는 추천 시스템을 운영한다고 생각해보세요. Docker 없이는 매번 서버 환경을 맞추는 데만 시간을 허비하게 됩니다.

하지만 Docker를 사용하면 새 모델을 컨테이너로 빌드하고 바로 배포할 수 있습니다. 주의할 점도 있습니다.

초보자들이 흔히 하는 실수 중 하나는 컨테이너 이미지를 너무 크게 만드는 것입니다. 불필요한 파일을 포함하거나, 무거운 베이스 이미지를 사용하면 빌드와 배포 시간이 길어집니다.

따라서 slim 이미지를 사용하고, 필요한 파일만 복사하는 습관을 들여야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언대로 Docker를 적용한 후, 운영팀 서버에서도 모델이 완벽하게 동작했습니다. "이제 환경 문제로 스트레스받을 일이 없겠네요!" 김개발 씨의 얼굴에 미소가 번졌습니다.

실전 팁

💡 - python:3.9-slim처럼 slim 버전의 베이스 이미지를 사용하면 컨테이너 크기를 크게 줄일 수 있습니다

  • .dockerignore 파일을 활용하여 불필요한 파일이 이미지에 포함되지 않도록 하세요

2. requirements.txt 작성과 의존성 관리

김개발 씨가 Docker 이미지를 빌드하려는데 에러가 발생했습니다. 분명히 로컬에서는 잘 실행되는 코드인데, 빌드 과정에서 라이브러리를 찾을 수 없다는 메시지가 나옵니다.

알고 보니 requirements.txt 파일에 필요한 패키지를 빠뜨린 것이었습니다.

requirements.txt는 Python 프로젝트에 필요한 모든 라이브러리와 버전을 명시하는 파일입니다. 마치 요리 레시피에 재료 목록을 적어두는 것과 같습니다.

이 파일이 정확해야 Docker 컨테이너 안에서도 모델이 올바르게 동작합니다.

다음 코드를 살펴봅시다.

# requirements.txt - ML 모델 의존성 목록
# 웹 프레임워크
flask==2.3.0
gunicorn==21.2.0

# 머신러닝 라이브러리
scikit-learn==1.3.0
pandas==2.0.3
numpy==1.24.3

# 모델 저장/로드
joblib==1.3.1

# 버전을 고정하면 재현성이 보장됩니다
# pip freeze > requirements.txt 로 현재 환경을 추출할 수 있습니다

박시니어 씨가 커피를 마시며 김개발 씨에게 물었습니다. "requirements.txt는 어떻게 관리하고 있어요?" 김개발 씨는 솔직하게 대답했습니다.

"그냥 필요할 때마다 패키지 이름만 추가했어요." 박시니어 씨가 고개를 저었습니다. "그러면 나중에 큰 문제가 생길 수 있어요.

제가 한번 겪은 일을 이야기해 드릴게요." 몇 달 전, 운영 중이던 서비스가 갑자기 오류를 내뿜기 시작했습니다. 코드는 한 줄도 바꾸지 않았는데 말입니다.

원인을 추적해 보니 서버를 재시작하면서 라이브러리가 새 버전으로 업데이트된 것이 문제였습니다. 새 버전에서 일부 함수의 동작 방식이 바뀌어 버린 것입니다.

이런 문제를 방지하려면 버전을 정확히 고정해야 합니다. pandas라고만 적지 말고 pandas==2.0.3처럼 구체적인 버전을 명시해야 합니다.

이렇게 하면 언제 어디서 설치하든 항상 같은 버전이 설치됩니다. 쉽게 비유하자면, 요리할 때 "소금 약간"이라고 적으면 사람마다 다르게 해석합니다.

하지만 "소금 5그램"이라고 적으면 누가 만들어도 같은 맛이 납니다. requirements.txt의 버전 명시도 같은 원리입니다.

현재 개발 환경의 모든 패키지 버전을 추출하는 방법도 있습니다. pip freeze 명령어를 사용하면 설치된 모든 패키지와 버전이 출력됩니다.

이것을 파일로 저장하면 현재 환경을 그대로 복제할 수 있습니다. 다만 주의할 점이 있습니다.

pip freeze는 불필요한 패키지까지 모두 포함할 수 있습니다. 실제로 프로젝트에 필요한 핵심 패키지만 명시하고, 나머지는 의존성으로 자동 설치되게 하는 것이 깔끔합니다.

ML 프로젝트에서는 특히 scikit-learn, TensorFlow, PyTorch 같은 머신러닝 라이브러리의 버전 관리가 중요합니다. 이런 라이브러리들은 버전에 따라 모델 저장 형식이나 API가 달라질 수 있기 때문입니다.

모델을 학습할 때 사용한 버전과 추론할 때 사용하는 버전이 다르면 예상치 못한 오류가 발생할 수 있습니다. 김개발 씨는 박시니어 씨의 조언을 듣고 requirements.txt를 다시 정리했습니다.

핵심 패키지들의 버전을 모두 고정하고, 주석으로 각 패키지의 용도도 적어두었습니다. "이제 팀원 누구나 이 파일만 보면 무슨 라이브러리가 왜 필요한지 알 수 있겠네요."

실전 팁

💡 - pip freeze > requirements.txt로 현재 환경을 추출하되, 필요 없는 패키지는 정리하세요

  • 주석을 활용해 각 패키지의 용도를 명시하면 유지보수가 편해집니다

3. Flask API로 모델 서빙하기

김개발 씨의 모델이 컨테이너에 잘 담겼습니다. 하지만 한 가지 고민이 생겼습니다.

"이 모델을 다른 서비스에서 어떻게 호출하지?" 모델을 학습하는 것과 실제로 서비스에서 사용하는 것은 완전히 다른 문제였습니다.

Flask는 Python으로 웹 서버를 간단하게 만들 수 있는 프레임워크입니다. 마치 식당에서 손님의 주문을 받고 음식을 내어주는 종업원과 같습니다.

ML 모델을 Flask API로 감싸면 HTTP 요청으로 누구나 예측 결과를 받아볼 수 있습니다.

다음 코드를 살펴봅시다.

# app.py - ML 모델을 API로 서빙하는 Flask 애플리케이션
from flask import Flask, request, jsonify
import joblib
import numpy as np

app = Flask(__name__)

# 모델 로드 (서버 시작 시 한 번만 실행)
model = joblib.load('model/classifier.pkl')

@app.route('/predict', methods=['POST'])
def predict():
    # 요청에서 입력 데이터 추출
    data = request.json['features']
    features = np.array(data).reshape(1, -1)

    # 모델로 예측 수행
    prediction = model.predict(features)

    return jsonify({'prediction': prediction.tolist()})

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

박시니어 씨가 화이트보드 앞에 섰습니다. "자, ML 모델을 서비스에 연동하는 가장 일반적인 방법을 알려줄게요." 김개발 씨는 노트를 펼치고 집중했습니다.

모델을 학습하는 것과 서비스에서 사용하는 것은 완전히 다른 영역입니다. 학습은 Jupyter Notebook에서 하더라도, 실제 서비스에서는 HTTP API 형태로 모델을 호출하는 경우가 많습니다.

프론트엔드 개발자도, 백엔드 개발자도 HTTP 요청은 쉽게 보낼 수 있기 때문입니다. Flask는 Python으로 웹 서버를 만드는 가장 간단한 방법 중 하나입니다.

몇 줄의 코드로 API 엔드포인트를 만들 수 있습니다. 물론 대규모 서비스에서는 FastAPI나 다른 프레임워크를 사용하기도 하지만, 기본 원리를 배우기에는 Flask가 좋습니다.

위 코드를 살펴보겠습니다. 먼저 서버가 시작될 때 model = joblib.load() 부분에서 학습된 모델을 메모리에 로드합니다.

이렇게 하면 매 요청마다 모델을 다시 로드할 필요가 없어 응답 속도가 빨라집니다. /predict 엔드포인트는 POST 요청을 받습니다.

클라이언트가 JSON 형태로 입력 데이터를 보내면, 모델이 예측을 수행하고 결과를 JSON으로 반환합니다. 이것이 바로 REST API의 기본 패턴입니다.

host='0.0.0.0'으로 설정하는 것이 중요합니다. 이렇게 해야 Docker 컨테이너 외부에서도 접속할 수 있습니다.

localhost나 127.0.0.1로 설정하면 컨테이너 내부에서만 접근 가능합니다. 실제 현업에서는 이 API를 다양한 곳에서 호출합니다.

웹 서비스에서 실시간 추천을 보여주거나, 모바일 앱에서 사진을 분석하거나, 다른 백엔드 서비스에서 사기 탐지를 수행하는 식입니다. 모델이 API로 제공되면 활용 범위가 무궁무진해집니다.

김개발 씨가 질문했습니다. "그런데 이 Flask 서버로 트래픽이 많이 몰리면 어떻게 되나요?" 좋은 질문입니다.

Flask 기본 서버는 개발용이라 성능이 제한적입니다. 다음 카드에서 이 문제를 해결하는 방법을 다루겠습니다.

실전 팁

💡 - 모델 로드는 서버 시작 시 한 번만 수행하여 응답 속도를 최적화하세요

  • host='0.0.0.0'으로 설정해야 Docker 컨테이너 외부에서 접근할 수 있습니다

4. Gunicorn으로 프로덕션 서버 구성하기

김개발 씨가 Flask 서버를 띄우고 테스트를 해봤습니다. 한 번에 하나의 요청은 잘 처리됩니다.

그런데 동시에 여러 요청이 들어오면 응답이 느려지기 시작합니다. 박시니어 씨가 말했습니다.

"Flask 개발 서버로는 운영 환경을 감당할 수 없어요. Gunicorn을 써야 합니다."

Gunicorn은 Python 웹 애플리케이션을 위한 프로덕션급 WSGI 서버입니다. 마치 식당에서 종업원 한 명이 모든 손님을 응대하는 대신, 여러 명의 종업원이 동시에 일하는 것과 같습니다.

여러 워커 프로세스가 요청을 병렬로 처리하여 동시 접속을 효율적으로 처리합니다.

다음 코드를 살펴봅시다.

# Dockerfile - Gunicorn으로 프로덕션 서버 실행
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY model/ ./model/
COPY app.py .

# 포트 노출 선언
EXPOSE 5000

# Gunicorn으로 실행 (워커 4개, 타임아웃 120초)
CMD ["gunicorn", "--workers", "4", "--timeout", "120", \
     "--bind", "0.0.0.0:5000", "app:app"]

박시니어 씨가 화면을 가리키며 설명했습니다. "Flask 내장 서버가 왜 운영에 적합하지 않은지 보여줄게요." Flask를 python app.py로 실행하면 개발 서버가 뜹니다.

이 서버는 단일 스레드로 동작합니다. 한 번에 하나의 요청만 처리할 수 있다는 뜻입니다.

첫 번째 요청을 처리하는 동안 두 번째 요청은 기다려야 합니다. ML 모델 추론은 시간이 걸리는 작업입니다.

복잡한 모델이라면 한 번의 예측에 수백 밀리초가 걸릴 수 있습니다. 그 동안 다른 사용자들은 모두 대기해야 합니다.

사용자가 늘어나면 응답 시간이 급격히 나빠집니다. Gunicorn은 이 문제를 해결합니다.

마치 대형 마트에서 계산대를 여러 개 열어두는 것과 같습니다. 워커 프로세스를 여러 개 띄워서 각 워커가 독립적으로 요청을 처리합니다.

4개의 워커가 있다면 동시에 4개의 요청을 처리할 수 있습니다. 위 Dockerfile에서 --workers 4는 워커 프로세스를 4개 생성하라는 의미입니다.

일반적으로 CPU 코어 수의 2배 + 1을 권장합니다. 2코어 서버라면 5개의 워커가 적당합니다.

--timeout 120은 중요한 설정입니다. ML 모델은 일반 웹 요청보다 처리 시간이 길 수 있습니다.

기본 타임아웃인 30초 안에 응답하지 못하면 Gunicorn이 워커를 강제로 재시작합니다. 모델 특성에 따라 적절한 타임아웃을 설정해야 합니다.

EXPOSE 5000은 Docker에게 이 컨테이너가 5000번 포트를 사용한다고 알려줍니다. 이것은 문서화 목적이며, 실제 포트 매핑은 docker run 명령에서 설정합니다.

김개발 씨가 고개를 끄덕였습니다. "그러면 트래픽이 더 늘어나면 워커 수를 늘리면 되겠네요?" 박시니어 씨가 대답했습니다.

"맞아요. 하지만 서버의 메모리와 CPU 한계도 고려해야 해요.

각 워커가 모델을 메모리에 로드하니까, 워커가 많아질수록 메모리 사용량도 늘어납니다."

실전 팁

💡 - 워커 수는 보통 CPU 코어 수의 2배 + 1로 설정합니다

  • ML 모델 추론은 시간이 걸리므로 timeout 값을 충분히 여유있게 설정하세요

5. Docker 이미지 빌드와 실행

Dockerfile도 작성했고, 코드도 준비됐습니다. 이제 실제로 Docker 이미지를 빌드하고 컨테이너를 실행할 차례입니다.

김개발 씨는 터미널을 열고 박시니어 씨가 알려준 명령어를 하나씩 입력했습니다.

Docker 빌드는 Dockerfile을 바탕으로 이미지를 생성하는 과정입니다. 마치 설계도를 보고 집을 짓는 것과 같습니다.

빌드된 이미지로 컨테이너를 실행하면 어떤 서버에서든 동일한 환경에서 모델이 동작합니다.

다음 코드를 살펴봅시다.

# 터미널에서 Docker 이미지 빌드 및 실행
# 1. 이미지 빌드 (-t 옵션으로 이름 지정)
docker build -t ml-model-api:v1.0 .

# 2. 빌드된 이미지 확인
docker images | grep ml-model-api

# 3. 컨테이너 실행 (-d: 백그라운드, -p: 포트 매핑)
docker run -d --name ml-api -p 8080:5000 ml-model-api:v1.0

# 4. 실행 중인 컨테이너 확인
docker ps

# 5. API 테스트
curl -X POST http://localhost:8080/predict \
  -H "Content-Type: application/json" \
  -d '{"features": [5.1, 3.5, 1.4, 0.2]}'

김개발 씨가 터미널에서 docker build 명령을 실행했습니다. 화면에 빌드 과정이 줄줄이 출력됩니다.

Python 이미지 다운로드, 라이브러리 설치, 파일 복사... 모든 과정이 자동으로 진행됩니다.

docker build -t ml-model-api:v1.0 . 명령을 분석해 봅시다.

-t 옵션은 이미지에 이름과 태그를 부여합니다. ml-model-api가 이름이고 v1.0이 태그입니다.

버전을 명시하면 나중에 어떤 버전의 모델인지 쉽게 구분할 수 있습니다. 마지막의 점(.)은 현재 디렉토리를 빌드 컨텍스트로 사용하라는 의미입니다.

Docker는 이 디렉토리의 Dockerfile을 찾아 빌드를 수행합니다. 빌드가 완료되면 docker images 명령으로 생성된 이미지를 확인할 수 있습니다.

이미지 크기도 함께 표시됩니다. ML 모델이 포함되어 있으니 일반 웹 애플리케이션보다 크기가 클 수 있습니다.

이제 docker run 명령으로 컨테이너를 실행합니다. -d 옵션은 백그라운드에서 실행하라는 의미입니다.

터미널을 점유하지 않고 뒤에서 조용히 돌아갑니다. --name ml-api로 컨테이너에 이름을 붙여두면 나중에 관리하기 편합니다.

-p 8080:5000은 포트 매핑입니다. 호스트의 8080 포트로 들어오는 요청을 컨테이너의 5000 포트로 전달합니다.

왜 다른 포트를 쓸까요? 호스트에서 이미 5000 포트를 다른 서비스가 사용 중일 수 있기 때문입니다.

포트 매핑을 통해 유연하게 조정할 수 있습니다. 마지막으로 curl 명령으로 API를 테스트합니다.

JSON 데이터를 POST로 전송하고 예측 결과를 받습니다. {"prediction": [0]}과 같은 응답이 오면 성공입니다.

김개발 씨가 환호했습니다. "진짜 된다!

제가 만든 모델이 API로 동작하고 있어요!" 박시니어 씨가 미소 지었습니다. "축하해요.

이제 이 이미지만 있으면 어떤 서버에서든 동일하게 실행할 수 있어요."

실전 팁

💡 - 이미지 태그에 버전을 명시하여 모델 버전을 관리하세요

  • 포트 매핑(-p 호스트:컨테이너)으로 원하는 포트에서 서비스할 수 있습니다

6. 멀티스테이지 빌드로 이미지 최적화

김개발 씨가 빌드한 이미지 크기를 확인했더니 2GB가 넘었습니다. "이렇게 크면 배포할 때 시간이 너무 오래 걸리지 않나요?" 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 이미지를 가볍게 만드는 기술이 있어요.

멀티스테이지 빌드라고 합니다."

멀티스테이지 빌드는 여러 단계로 이미지를 빌드하여 최종 이미지에는 실행에 필요한 것만 포함시키는 기술입니다. 마치 요리할 때 필요한 재료만 접시에 담고 도마와 칼은 설거지하는 것과 같습니다.

빌드 도구나 임시 파일을 제외하여 이미지 크기를 대폭 줄입니다.

다음 코드를 살펴봅시다.

# Dockerfile - 멀티스테이지 빌드로 이미지 최적화
# 1단계: 빌드 스테이지
FROM python:3.9-slim AS builder

WORKDIR /app
COPY requirements.txt .

# 가상환경 생성 및 패키지 설치
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt

# 2단계: 실행 스테이지 (최종 이미지)
FROM python:3.9-slim

WORKDIR /app

# 빌드 스테이지에서 가상환경만 복사
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY model/ ./model/
COPY app.py .

EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]

박시니어 씨가 설명을 시작했습니다. "Docker 이미지가 큰 이유를 생각해 본 적 있어요?" 일반적인 빌드 과정에서는 라이브러리를 설치하면서 많은 임시 파일이 생성됩니다.

컴파일러, 빌드 도구, 캐시 파일 등이 쌓입니다. 이것들은 빌드할 때만 필요하고 실제 실행할 때는 필요 없습니다.

하지만 단일 스테이지 빌드에서는 이 모든 것이 최종 이미지에 포함됩니다. 멀티스테이지 빌드는 이 문제를 해결합니다.

이름처럼 여러 단계로 빌드를 나누는 것입니다. 첫 번째 단계에서 필요한 모든 것을 설치하고, 두 번째 단계에서는 실행에 필요한 결과물만 가져옵니다.

위 Dockerfile을 살펴보겠습니다. FROM python:3.9-slim AS builder가 첫 번째 스테이지입니다.

builder라는 이름을 붙였습니다. 이 단계에서 가상환경을 만들고 모든 패키지를 설치합니다.

두 번째 FROM python:3.9-slim이 실제 최종 이미지가 됩니다. COPY --from=builder 구문이 핵심입니다.

이 명령은 builder 스테이지에서 /opt/venv 디렉토리만 복사해 옵니다. 빌드 과정에서 생긴 캐시나 임시 파일은 버려집니다.

마치 이사할 때 필요한 짐만 새 집으로 옮기는 것과 같습니다. 오래된 영수증이나 빈 상자는 가져가지 않습니다.

필요한 것만 가져가니 새 집이 깔끔해집니다. 이 기법을 적용하면 이미지 크기가 절반 이하로 줄어들 수 있습니다.

작은 이미지는 여러 장점이 있습니다. 빌드 시간이 단축되고, 저장소에 푸시하는 시간도 줄어들고, 새 서버에 배포할 때 다운로드 시간도 빨라집니다.

특히 ML 모델 이미지는 크기가 커지기 쉽습니다. 과학 계산 라이브러리들이 무거운 편이기 때문입니다.

멀티스테이지 빌드를 적극 활용해야 하는 이유입니다. 김개발 씨가 멀티스테이지 빌드를 적용하고 다시 이미지를 빌드했습니다.

크기가 2GB에서 800MB로 줄었습니다. "절반 넘게 줄었네요!"

실전 팁

💡 - AS 키워드로 빌드 스테이지에 이름을 붙이고 --from으로 참조합니다

  • 가상환경을 활용하면 필요한 패키지만 깔끔하게 복사할 수 있습니다

7. 환경변수로 설정 분리하기

김개발 씨가 개발 환경과 운영 환경에서 서로 다른 설정을 사용해야 하는 상황에 놓였습니다. 개발할 때는 디버그 모드로, 운영에서는 최적화 모드로 실행하고 싶습니다.

코드를 두 벌 만들어야 할까요? 박시니어 씨가 더 좋은 방법을 알려줬습니다.

환경변수는 컨테이너 외부에서 애플리케이션의 동작을 제어하는 설정값입니다. 마치 TV 리모컨처럼 본체를 열지 않고도 채널이나 볼륨을 바꿀 수 있습니다.

같은 이미지로 환경에 따라 다른 설정을 적용할 수 있어 유연한 배포가 가능합니다.

다음 코드를 살펴봅시다.

# app.py - 환경변수를 활용한 설정 관리
import os
from flask import Flask, request, jsonify
import joblib

app = Flask(__name__)

# 환경변수에서 설정 읽기 (기본값 제공)
MODEL_PATH = os.getenv('MODEL_PATH', 'model/classifier.pkl')
DEBUG_MODE = os.getenv('DEBUG', 'false').lower() == 'true'
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')

# 모델 로드
model = joblib.load(MODEL_PATH)

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json['features']
    prediction = model.predict([data])
    return jsonify({'prediction': prediction.tolist()})

# 컨테이너 실행 예시:
# docker run -e MODEL_PATH=/app/models/v2.pkl \
#            -e DEBUG=true -p 8080:5000 ml-model-api:v1.0

박시니어 씨가 질문했습니다. "개발 환경과 운영 환경에서 모델 경로가 다르면 어떻게 할 거예요?

코드를 수정해서 다시 빌드할 건가요?" 김개발 씨는 잠시 생각했습니다. 그렇게 하면 환경마다 다른 이미지를 관리해야 합니다.

실수하기도 쉽고 번거롭습니다. 분명 더 좋은 방법이 있을 것입니다.

환경변수가 바로 그 해답입니다. 코드 안에 설정값을 직접 적지 않고, 외부에서 주입받는 방식입니다.

마치 자동차의 연료 탱크처럼, 차는 그대로인데 휘발유를 넣느냐 경유를 넣느냐에 따라 다르게 동작하는 것과 비슷합니다. os.getenv() 함수가 핵심입니다.

이 함수는 환경변수의 값을 읽어옵니다. 첫 번째 인자가 환경변수 이름이고, 두 번째 인자가 기본값입니다.

환경변수가 설정되지 않으면 기본값을 사용합니다. 위 코드에서 MODEL_PATH는 환경변수로 전달할 수 있습니다.

개발 환경에서는 로컬의 모델 파일을, 운영 환경에서는 최적화된 모델 파일을 사용할 수 있습니다. 코드는 동일하지만 동작이 달라집니다.

docker run 명령에서 -e 옵션으로 환경변수를 전달합니다. -e MODEL_PATH=/app/models/v2.pkl처럼 키=값 형태로 지정합니다.

여러 개의 환경변수를 전달하려면 -e 옵션을 여러 번 사용하면 됩니다. 이 패턴은 12-Factor App 방법론에서도 권장하는 방식입니다.

설정을 코드와 분리하면 보안도 좋아집니다. API 키나 데이터베이스 비밀번호 같은 민감한 정보를 코드에 넣지 않아도 됩니다.

김개발 씨가 환경변수 패턴을 적용했습니다. 이제 같은 이미지로 개발, 스테이징, 운영 환경에 모두 배포할 수 있습니다.

"한 번 빌드해서 어디든 쓸 수 있으니까 편하네요!"

실전 팁

💡 - 민감한 정보(API 키, 비밀번호)는 반드시 환경변수로 관리하세요

  • 기본값을 제공하면 환경변수 설정을 잊어도 에러 없이 동작합니다

8. 헬스체크로 컨테이너 상태 모니터링

어느 날 운영팀에서 연락이 왔습니다. "모델 API가 응답이 없어요!" 김개발 씨가 확인해 보니 컨테이너는 실행 중이었습니다.

하지만 내부의 Flask 서버가 메모리 부족으로 멈춰 있었습니다. 컨테이너가 살아있는지와 서비스가 정상인지는 다른 문제였습니다.

헬스체크는 컨테이너 내부의 애플리케이션이 정상적으로 동작하는지 주기적으로 확인하는 기능입니다. 마치 의사가 환자의 맥박을 체크하는 것과 같습니다.

컨테이너 자체는 실행 중이더라도 내부 서비스가 문제가 있으면 감지하고 대응할 수 있습니다.

다음 코드를 살펴봅시다.

# Dockerfile - 헬스체크 설정 추가
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY model/ ./model/
COPY app.py .

# 헬스체크 엔드포인트를 30초마다 호출
# 3초 안에 응답 없으면 실패, 3회 연속 실패 시 unhealthy
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:5000/health || exit 1

EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]

# app.py에 헬스체크 엔드포인트 추가
# @app.route('/health')
# def health():
#     return jsonify({'status': 'healthy'})

박시니어 씨가 상황을 설명했습니다. "컨테이너가 실행 중이라고 해서 서비스가 정상인 건 아니에요.

안에서 뭔가 잘못될 수 있거든요." Docker는 기본적으로 프로세스가 살아있는지만 확인합니다. 메인 프로세스가 종료되면 컨테이너도 종료됩니다.

하지만 프로세스가 살아있어도 실제로 요청을 처리하지 못하는 경우가 있습니다. 메모리 누수로 느려지거나, 데드락에 빠지거나, 외부 의존성 문제로 응답하지 못할 수 있습니다.

HEALTHCHECK 명령어는 이런 상황을 감지합니다. 주기적으로 지정된 명령을 실행하여 성공/실패를 판단합니다.

위 설정을 분석해 보겠습니다. --interval=30s는 30초마다 체크한다는 의미입니다.

너무 자주 체크하면 부하가 되고, 너무 드물게 체크하면 문제 감지가 늦어집니다. 30초가 적당한 기본값입니다.

--timeout=3s는 응답을 3초까지 기다린다는 뜻입니다. 정상적인 헬스체크 엔드포인트라면 즉시 응답해야 합니다.

3초 안에 응답이 없으면 문제가 있다고 판단합니다. --retries=3은 3번 연속 실패해야 unhealthy로 판정한다는 의미입니다.

일시적인 네트워크 지연 같은 상황에서 과민 반응하지 않도록 합니다. curl -f http://localhost:5000/health 명령이 실제 체크 로직입니다.

/health 엔드포인트를 호출하여 응답을 확인합니다. -f 옵션은 HTTP 에러 시 실패 코드를 반환하게 합니다.

애플리케이션에도 헬스체크 엔드포인트를 만들어야 합니다. 단순히 200 OK를 반환하는 것부터, 데이터베이스 연결을 확인하거나 모델 로드 상태를 체크하는 등 다양하게 구현할 수 있습니다.

쿠버네티스 같은 오케스트레이션 도구에서는 헬스체크를 활용하여 문제가 있는 컨테이너를 자동으로 재시작합니다. 운영 환경에서 안정성을 높이는 핵심 기능입니다.

실전 팁

💡 - 헬스체크 엔드포인트는 빠르게 응답해야 합니다. 복잡한 로직은 피하세요

  • 모델 로드 상태나 메모리 사용량 등 실제 서비스 가능 여부를 확인하세요

9. Docker Compose로 다중 컨테이너 관리

김개발 씨의 프로젝트가 커졌습니다. 이제 ML API 외에도 Redis 캐시와 모니터링 서비스가 필요합니다.

매번 docker run 명령을 여러 번 입력하고 네트워크 설정을 해주는 게 번거롭습니다. 박시니어 씨가 말했습니다.

"Docker Compose를 써보세요."

Docker Compose는 여러 컨테이너를 하나의 파일로 정의하고 한 번에 실행할 수 있는 도구입니다. 마치 오케스트라 지휘자가 여러 악기를 조화롭게 이끄는 것과 같습니다.

복잡한 명령어 없이 YAML 파일 하나로 전체 서비스 스택을 관리합니다.

다음 코드를 살펴봅시다.

# docker-compose.yml - 다중 컨테이너 서비스 정의
version: '3.8'

services:
  ml-api:
    build: .
    ports:
      - "8080:5000"
    environment:
      - MODEL_PATH=/app/model/classifier.pkl
      - REDIS_HOST=redis
    depends_on:
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      retries: 3

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

# 실행: docker-compose up -d
# 중지: docker-compose down
# 로그: docker-compose logs -f ml-api

박시니어 씨가 화이트보드에 서비스 구성도를 그렸습니다. "ML API가 Redis를 캐시로 사용한다고 해봐요.

두 컨테이너를 따로 실행하고 네트워크로 연결해야 해요. 명령어가 복잡해지죠." docker run 명령을 여러 번 입력하는 것은 번거로울 뿐 아니라 실수하기도 쉽습니다.

포트 번호를 잘못 쓰거나, 네트워크 이름을 틀리거나, 환경변수를 빠뜨릴 수 있습니다. 팀원에게 전달할 때도 문서로 정리해야 합니다.

Docker Compose는 이 모든 것을 YAML 파일 하나에 담습니다. 누구나 docker-compose up 한 줄로 동일한 환경을 구성할 수 있습니다.

위 docker-compose.yml을 살펴보겠습니다. services 아래에 각 컨테이너를 정의합니다.

ml-api 서비스는 현재 디렉토리의 Dockerfile을 빌드하여 사용합니다. redis 서비스는 Docker Hub의 공식 Redis 이미지를 사용합니다.

depends_on은 서비스 간의 의존성을 정의합니다. ml-api는 redis에 의존하므로, Compose가 redis를 먼저 시작합니다.

다만 이것은 시작 순서만 보장하고, redis가 완전히 준비될 때까지 기다리지는 않습니다. environment에서 REDIS_HOST=redis로 설정한 것에 주목하세요.

Compose는 자동으로 내부 네트워크를 구성하고, 서비스 이름을 호스트명으로 사용할 수 있게 합니다. ml-api 컨테이너에서 redis라는 이름으로 Redis 컨테이너에 접근할 수 있습니다.

docker-compose up -d로 모든 서비스를 백그라운드로 실행합니다. docker-compose logs -f로 로그를 실시간으로 확인할 수 있습니다.

docker-compose down으로 모든 서비스를 한 번에 중지하고 정리합니다. 개발 환경에서 특히 유용합니다.

새로운 팀원이 합류해도 docker-compose up 한 줄이면 전체 개발 환경이 구성됩니다. "내 컴퓨터에서는 되는데"라는 말이 사라집니다.

실전 팁

💡 - 서비스 이름을 호스트명으로 사용할 수 있어 네트워크 설정이 간편합니다

  • docker-compose.override.yml로 개발 환경만의 설정을 분리할 수 있습니다

10. 컨테이너 레지스트리에 이미지 배포하기

김개발 씨가 만든 이미지를 운영 서버에 배포해야 합니다. 로컬에서 빌드한 이미지를 USB에 담아 들고 갈 수는 없습니다.

박시니어 씨가 말했습니다. "Docker Hub 같은 레지스트리에 이미지를 푸시하면 어디서든 가져다 쓸 수 있어요."

컨테이너 레지스트리는 Docker 이미지를 저장하고 공유하는 저장소입니다. 마치 앱스토어처럼 이미지를 업로드하고 다운로드할 수 있습니다.

Docker Hub, AWS ECR, GCP GCR 등 다양한 레지스트리 서비스가 있습니다.

다음 코드를 살펴봅시다.

# 컨테이너 레지스트리에 이미지 푸시하기
# 1. Docker Hub 로그인
docker login

# 2. 이미지에 레지스트리 경로가 포함된 태그 붙이기
# 형식: 레지스트리주소/리포지토리이름:태그
docker tag ml-model-api:v1.0 myusername/ml-model-api:v1.0

# 3. 레지스트리에 푸시
docker push myusername/ml-model-api:v1.0

# 4. 다른 서버에서 이미지 가져오기
docker pull myusername/ml-model-api:v1.0

# 5. AWS ECR 사용 시 (사전에 ECR 리포지토리 생성 필요)
# aws ecr get-login-password | docker login --username AWS \
#   --password-stdin 123456789.dkr.ecr.ap-northeast-2.amazonaws.com
# docker tag ml-model-api:v1.0 \
#   123456789.dkr.ecr.ap-northeast-2.amazonaws.com/ml-api:v1.0
# docker push 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/ml-api:v1.0

지금까지 김개발 씨는 로컬에서만 이미지를 빌드하고 테스트했습니다. 하지만 실제 서비스는 운영 서버에서 돌아갑니다.

로컬의 이미지를 어떻게 운영 서버로 옮길까요? 컨테이너 레지스트리가 그 답입니다.

Git으로 코드를 GitHub에 올리듯, Docker 이미지를 레지스트리에 올립니다. 운영 서버에서는 레지스트리에서 이미지를 다운로드하여 실행합니다.

Docker Hub는 가장 널리 알려진 퍼블릭 레지스트리입니다. 무료로 사용할 수 있고, 공개 이미지는 누구나 받아갈 수 있습니다.

비공개 이미지도 일정 수까지는 무료입니다. 이미지를 푸시하려면 먼저 태그를 적절히 붙여야 합니다.

myusername/ml-model-api:v1.0처럼 레지스트리 경로가 포함된 형태여야 합니다. docker tag 명령으로 기존 이미지에 새 태그를 붙일 수 있습니다.

docker push 명령으로 이미지를 업로드합니다. 이미지 레이어가 하나씩 업로드되는 것을 볼 수 있습니다.

이미 존재하는 레이어는 스킵되어 시간이 절약됩니다. 기업 환경에서는 프라이빗 레지스트리를 많이 사용합니다.

AWS ECR, GCP GCR, Azure ACR 등 클라우드 제공 서비스가 있고, Harbor 같은 오픈소스 솔루션도 있습니다. 보안과 접근 제어가 중요한 상황에서 유용합니다.

CI/CD 파이프라인에서 이 과정을 자동화합니다. 코드가 푸시되면 자동으로 이미지를 빌드하고, 테스트를 통과하면 레지스트리에 푸시합니다.

운영 서버는 새 이미지를 감지하여 자동으로 배포합니다. 김개발 씨가 Docker Hub에 이미지를 푸시했습니다.

운영팀에서 docker pull 명령 한 줄로 이미지를 받아 실행했습니다. "이제 배포가 정말 간단해졌네요!"

실전 팁

💡 - 이미지 태그에 git commit hash를 포함하면 어떤 코드 버전인지 추적하기 쉽습니다

  • 프로덕션 이미지는 latest 태그 대신 명시적인 버전 태그를 사용하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Docker#컨테이너화#MLOps#모델배포#DevOps#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Helm 마이크로서비스 패키징 완벽 가이드

Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.

EFK 스택 로깅 완벽 가이드

마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.

Spring Boot 상품 서비스 구축 완벽 가이드

실무 RESTful API 설계부터 테스트, 배포까지 Spring Boot로 상품 서비스를 만드는 전 과정을 다룹니다. JPA 엔티티 설계, OpenAPI 문서화, Docker Compose 배포 전략을 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링으로 풀어냅니다.

Docker로 컨테이너화 완벽 가이드

Spring Boot 애플리케이션을 Docker로 컨테이너화하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. Dockerfile 작성부터 멀티스테이지 빌드, 이미지 최적화, Spring Boot의 Buildpacks까지 다룹니다.

보안 아키텍처 구성 완벽 가이드

프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.