본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 2. · 15 Views
실전 프로젝트 이미지 분류 서비스 완벽 가이드
이미지 분류 서비스를 처음부터 끝까지 직접 만들어보는 실전 프로젝트입니다. 데이터 수집부터 모델 학습, FastAPI를 활용한 API 구축, Docker 컨테이너화까지 전 과정을 다룹니다.
목차
1. 프로젝트 기획 및 데이터 수집
김개발 씨는 스타트업에 입사한 지 6개월 된 주니어 개발자입니다. 어느 날 팀장님이 다가와 말했습니다.
"우리 쇼핑몰에 올라오는 상품 이미지를 자동으로 분류하는 시스템을 만들어 볼래요?" 김개발 씨는 고개를 끄덕였지만 속으로는 막막했습니다. 도대체 어디서부터 시작해야 할까요?
이미지 분류 프로젝트는 기획 단계에서 성패가 결정됩니다. 어떤 카테고리로 분류할지, 데이터는 어디서 얻을지, 각 클래스당 몇 장의 이미지가 필요한지를 먼저 정해야 합니다.
마치 건물을 짓기 전에 설계도를 그리는 것처럼, 프로젝트 기획은 모든 것의 토대가 됩니다.
다음 코드를 살펴봅시다.
import os
import requests
from pathlib import Path
# 프로젝트 디렉토리 구조 생성
def create_project_structure(base_path: str, categories: list):
"""데이터 수집을 위한 폴더 구조를 생성합니다"""
dirs = ['data/raw', 'data/processed', 'models', 'logs']
for dir_name in dirs:
Path(f"{base_path}/{dir_name}").mkdir(parents=True, exist_ok=True)
# 각 카테고리별 폴더 생성
for category in categories:
Path(f"{base_path}/data/raw/{category}").mkdir(exist_ok=True)
print(f"프로젝트 구조 생성 완료: {categories}")
# 사용 예시
categories = ['의류', '전자기기', '식품', '가구']
create_project_structure('./image_classifier', categories)
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 오늘 팀장님으로부터 이미지 분류 시스템 개발이라는 큰 과제를 받았습니다.
머신러닝은 학교에서 배웠지만, 실제 프로젝트는 처음이라 어디서부터 손을 대야 할지 막막합니다. 선배 개발자 박시니어 씨가 커피를 건네며 다가왔습니다.
"처음이라 막막하지? 일단 기획부터 제대로 해야 해.
데이터 없이는 아무것도 못 해." 그렇다면 이미지 분류 프로젝트 기획이란 정확히 무엇일까요? 쉽게 비유하자면, 기획은 마치 여행 계획을 세우는 것과 같습니다.
어디로 갈지(분류 카테고리), 무엇을 준비할지(데이터), 어떤 교통수단을 이용할지(모델 아키텍처)를 미리 정해두지 않으면 여행 중에 헤매기 쉽습니다. 이미지 분류 프로젝트도 마찬가지입니다.
기획 없이 바로 코딩부터 시작하면 어떤 일이 벌어질까요? 김개발 씨의 동기인 이급해 씨가 딱 그랬습니다.
"일단 코드부터 짜보자!"라며 달려들었다가 데이터가 부족해서 모델 성능이 안 나왔습니다. 카테고리를 중간에 바꾸느라 처음부터 다시 시작해야 했습니다.
결국 예상보다 두 배나 긴 시간이 걸렸습니다. 바로 이런 문제를 피하기 위해 체계적인 기획이 필요합니다.
먼저 분류할 카테고리를 명확히 정의해야 합니다. 쇼핑몰이라면 의류, 전자기기, 식품, 가구 같은 대분류부터 시작합니다.
카테고리 간 경계가 모호하면 모델도 헷갈립니다. "이 가방은 패션 소품인가, 여행용품인가?" 같은 애매한 상황을 미리 정리해 두어야 합니다.
다음으로 데이터 수집 전략을 세웁니다. 클래스당 최소 500장, 이상적으로는 1000장 이상의 이미지가 필요합니다.
데이터가 적으면 모델이 과적합되기 쉽고, 클래스 간 데이터 양이 불균형하면 편향된 예측을 하게 됩니다. 위의 코드를 살펴보겠습니다.
create_project_structure 함수는 프로젝트의 뼈대를 만듭니다. data/raw에는 원본 이미지가, data/processed에는 전처리된 이미지가 저장됩니다.
각 카테고리별 하위 폴더를 자동으로 생성하여 데이터를 체계적으로 관리할 수 있습니다. 실제 현업에서는 어떻게 데이터를 수집할까요?
자체 보유 데이터가 있다면 가장 좋습니다. 없다면 Kaggle, ImageNet, Open Images 같은 공개 데이터셋을 활용합니다.
웹 크롤링도 방법이지만 저작권 문제를 반드시 확인해야 합니다. 많은 기업에서는 라벨링 업체에 외주를 주기도 합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 데이터 품질을 무시하는 것입니다.
양이 많아도 흐릿하거나 잘못 라벨링된 이미지가 섞여 있으면 모델 성능이 떨어집니다. "쓰레기가 들어가면 쓰레기가 나온다(Garbage in, Garbage out)"라는 격언을 기억하세요.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 꼼꼼히 기획을 마친 김개발 씨는 자신감이 생겼습니다.
"이제 뭘 해야 할지 명확해졌어요!" 체계적인 기획은 프로젝트의 절반을 성공시킨 것과 같습니다. 여러분도 코딩에 뛰어들기 전에 충분한 시간을 들여 기획해 보세요.
실전 팁
💡 - 클래스 간 데이터 불균형을 방지하기 위해 각 카테고리당 비슷한 수의 이미지를 수집하세요
- 데이터 수집 시 다양한 각도, 조명, 배경의 이미지를 포함해야 모델 일반화 성능이 좋아집니다
2. 데이터 전처리 파이프라인
김개발 씨는 열심히 수집한 5000장의 이미지를 앞에 두고 뿌듯해했습니다. 그런데 막상 학습을 시작하려니 문제가 생겼습니다.
어떤 이미지는 4000x3000 픽셀이고, 어떤 것은 640x480입니다. 형식도 JPG, PNG, WebP가 뒤섞여 있습니다.
"이걸 어떻게 정리하지?"
데이터 전처리는 원본 데이터를 모델이 학습할 수 있는 형태로 변환하는 과정입니다. 마치 요리 전에 재료를 씻고 다듬는 것처럼, 이미지도 크기 조정, 정규화, 증강 등의 과정을 거쳐야 합니다.
잘 전처리된 데이터는 모델 성능을 크게 향상시킵니다.
다음 코드를 살펴봅시다.
import torch
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
# 데이터 전처리 파이프라인 정의
train_transform = transforms.Compose([
transforms.Resize((224, 224)), # 크기 통일
transforms.RandomHorizontalFlip(p=0.5), # 좌우 반전으로 데이터 증강
transforms.RandomRotation(15), # 회전으로 다양성 추가
transforms.ColorJitter(brightness=0.2), # 밝기 변화
transforms.ToTensor(), # 텐서 변환
transforms.Normalize( # 정규화
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
# 데이터셋 로드 및 DataLoader 생성
train_dataset = datasets.ImageFolder('data/train', transform=train_transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
김개발 씨는 데이터 수집을 마치고 기쁜 마음으로 학습을 시작하려 했습니다. 그런데 터미널에 빨간 에러 메시지가 쏟아져 나왔습니다.
"RuntimeError: Input image size mismatch..." 뭔가 잘못된 것 같습니다. 박시니어 씨가 화면을 보더니 웃었습니다.
"전처리 안 했구나? 이미지 크기가 제각각이면 모델이 처리를 못 해." 그렇다면 데이터 전처리란 정확히 무엇일까요?
쉽게 비유하자면, 전처리는 마치 레고 블록을 맞추기 전에 모양과 크기를 정리하는 것과 같습니다. 서로 다른 크기의 블록이 섞여 있으면 조립이 불가능합니다.
이미지도 마찬가지로 신경망에 입력되기 전에 동일한 규격으로 맞춰줘야 합니다. 전처리 없이 학습을 시도하면 어떤 일이 벌어질까요?
크기가 다른 이미지를 그대로 넣으면 배치 처리가 불가능합니다. 정규화를 하지 않으면 학습이 불안정해지고 수렴 속도가 느려집니다.
데이터 양이 부족하면 과적합이 발생해 새로운 이미지를 제대로 분류하지 못합니다. 바로 이런 문제를 해결하기 위해 전처리 파이프라인을 구축합니다.
첫 번째 단계는 **크기 조정(Resize)**입니다. 모든 이미지를 224x224 픽셀로 통일합니다.
이 크기는 ImageNet으로 사전 학습된 모델들이 기대하는 표준 크기입니다. 너무 작으면 정보 손실이, 너무 크면 메모리 문제가 생깁니다.
두 번째 단계는 **데이터 증강(Data Augmentation)**입니다. 좌우 반전, 회전, 밝기 조절 등을 통해 하나의 이미지로 여러 변형을 만들어냅니다.
마치 한 사람의 사진을 다양한 각도에서 찍어 얼굴 인식 데이터를 늘리는 것과 같습니다. 세 번째 단계는 **정규화(Normalization)**입니다.
픽셀 값을 0255에서 01 사이로 변환하고, 평균과 표준편차로 스케일링합니다. 이렇게 하면 학습이 안정적으로 진행됩니다.
위의 코드를 한 줄씩 살펴보겠습니다. transforms.Compose는 여러 전처리 단계를 순서대로 연결합니다.
RandomHorizontalFlip은 50% 확률로 이미지를 좌우 반전시킵니다. Normalize의 mean과 std 값은 ImageNet 데이터셋의 통계치로, 사전 학습된 모델과 함께 사용할 때 효과적입니다.
실제 현업에서는 어떻게 활용할까요? 쇼핑몰 상품 이미지라면 배경 제거, 화이트밸런스 조정 같은 추가 전처리가 필요할 수 있습니다.
의료 이미지라면 윈도우 레벨링, 노이즈 제거 등 도메인 특화 전처리가 중요합니다. 전처리는 데이터 특성에 맞게 커스터마이징해야 합니다.
하지만 주의할 점도 있습니다. 과도한 데이터 증강은 오히려 독이 될 수 있습니다.
상품 이미지를 180도 회전시키면 실제로는 존재하지 않는 이상한 이미지가 됩니다. 또한 테스트 데이터에는 증강을 적용하면 안 됩니다.
실제 서비스 환경과 동일한 조건에서 평가해야 하기 때문입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
전처리 파이프라인을 구축한 후 다시 학습을 시작하니 에러 없이 잘 돌아갔습니다. "전처리가 이렇게 중요한 거였구나!" 잘 설계된 전처리 파이프라인은 모델 성능의 기반입니다.
데이터의 특성을 잘 파악하고, 적절한 전처리 전략을 수립하세요.
실전 팁
💡 - 훈련용과 검증용 전처리를 분리하세요. 검증/테스트에는 증강을 적용하지 않습니다
- ImageNet 사전학습 모델 사용 시 반드시 해당 정규화 값(mean, std)을 적용하세요
3. 모델 설계 및 학습
전처리까지 마친 김개발 씨는 드디어 모델을 만들 차례입니다. 그런데 CNN, ResNet, VGG, EfficientNet...
선택지가 너무 많습니다. "직접 모델을 설계해야 하나요?
아니면 이미 만들어진 걸 쓰는 게 좋을까요?" 김개발 씨는 고민에 빠졌습니다.
**전이 학습(Transfer Learning)**은 이미 학습된 모델을 새로운 문제에 적용하는 기법입니다. 마치 영어를 배운 사람이 스페인어를 더 빨리 배우는 것처럼, ImageNet으로 학습된 모델은 새로운 이미지 분류 문제도 빠르게 학습합니다.
처음부터 모델을 만드는 것보다 훨씬 효율적입니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
from torchvision import models
# 사전 학습된 ResNet18 모델 로드
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
# 마지막 분류층을 우리 문제에 맞게 수정 (4개 클래스)
num_classes = 4
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 손실 함수와 옵티마이저 설정
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 학습 루프
def train_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(loader)
김개발 씨는 모델 설계 앞에서 막막해졌습니다. 합성곱 신경망의 원리는 알지만, 실제로 처음부터 설계하려니 어디서 시작해야 할지 모르겠습니다.
박시니어 씨가 조언했습니다. "요즘 누가 처음부터 만들어?
전이 학습 써. ImageNet으로 학습된 모델을 가져다 쓰면 돼." 그렇다면 전이 학습이란 정확히 무엇일까요?
쉽게 비유하자면, 전이 학습은 마치 레고 조립 설명서를 참고하는 것과 같습니다. 백지에서 시작하는 것보다 기본 구조가 있으면 훨씬 빨리 완성할 수 있습니다.
이미 수백만 장의 이미지로 학습된 모델은 엣지, 텍스처, 패턴 같은 기본적인 특징을 이미 알고 있습니다. 전이 학습 없이 처음부터 학습하면 어떻게 될까요?
수천 장의 이미지로는 복잡한 CNN을 제대로 학습시키기 어렵습니다. 과적합이 심하게 발생하고, 학습에 몇 주가 걸릴 수도 있습니다.
GPU 비용도 어마어마하게 들어갑니다. 실제로 ImageNet 전체를 학습시키는 데는 최신 GPU로도 며칠이 걸립니다.
바로 이런 문제를 해결하기 위해 전이 학습을 사용합니다. 전이 학습에는 두 가지 전략이 있습니다.
첫째는 Feature Extraction으로, 사전 학습된 레이어는 고정하고 마지막 분류층만 학습합니다. 둘째는 Fine-tuning으로, 전체 모델을 낮은 학습률로 재학습합니다.
데이터가 적으면 전자를, 충분하면 후자를 선택합니다. 위의 코드를 살펴보겠습니다.
models.resnet18은 PyTorch에서 제공하는 사전 학습된 ResNet18 모델입니다. weights 파라미터로 ImageNet 가중치를 로드합니다.
model.fc는 마지막 완전연결층으로, 원래 1000개 클래스를 분류하던 것을 우리의 4개 클래스로 교체합니다. train_epoch 함수는 한 에포크 동안의 학습을 수행합니다.
**model.train()**은 드롭아웃과 배치 정규화를 학습 모드로 전환합니다. **optimizer.zero_grad()**로 기울기를 초기화하고, **loss.backward()**로 역전파하여 파라미터를 업데이트합니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 이미지 분류 프로젝트에서 전이 학습은 기본입니다.
ResNet, EfficientNet, ViT 같은 모델을 상황에 맞게 선택합니다. 속도가 중요하면 MobileNet을, 정확도가 중요하면 EfficientNet-B7을 사용합니다.
요즘은 Hugging Face에서 다양한 사전 학습 모델을 쉽게 가져다 쓸 수 있습니다. 하지만 주의할 점도 있습니다.
사전 학습 데이터와 우리 데이터의 도메인이 너무 다르면 전이 학습 효과가 떨어집니다. 예를 들어 자연 이미지로 학습된 모델을 의료 X-ray 이미지에 적용하면 Fine-tuning이 필수입니다.
또한 너무 큰 모델을 선택하면 추론 속도가 느려져 서비스에 부적합할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
전이 학습으로 모델을 구성하니 불과 10에포크 만에 90%가 넘는 정확도를 달성했습니다. "처음부터 만들었으면 한 달은 걸렸을 텐데!" 전이 학습은 현대 딥러닝의 핵심 기법입니다.
거인의 어깨 위에 서서 더 멀리 바라보세요.
실전 팁
💡 - 데이터가 1000장 미만이면 Feature Extraction 방식을, 그 이상이면 Fine-tuning을 고려하세요
- 학습률은 사전학습 레이어에는 낮게(1e-5), 새 레이어에는 높게(1e-3) 설정하는 것이 효과적입니다
4. 성능 평가 및 개선
모델 학습을 마친 김개발 씨는 뿌듯한 마음으로 팀장님께 보고했습니다. "정확도 95% 나왔습니다!" 그런데 팀장님의 반응은 의외였습니다.
"그 95%가 진짜 95%야? 클래스별 성능은 어때?
실제 서비스에서도 그렇게 나올까?"
모델 평가는 단순히 정확도 하나로 끝나지 않습니다. 정밀도, 재현율, F1-스코어, 혼동 행렬 등 다양한 지표를 종합적으로 분석해야 합니다.
마치 건강검진에서 여러 항목을 검사하는 것처럼, 모델의 건강 상태도 다각도로 점검해야 합니다.
다음 코드를 살펴봅시다.
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
def evaluate_model(model, test_loader, device, class_names):
"""모델 성능을 종합적으로 평가합니다"""
model.eval()
all_preds, all_labels = [], []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, preds = torch.max(outputs, 1)
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.numpy())
# 분류 리포트 출력
print(classification_report(all_labels, all_preds, target_names=class_names))
# 혼동 행렬 시각화
cm = confusion_matrix(all_labels, all_preds)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', xticklabels=class_names, yticklabels=class_names)
plt.title('Confusion Matrix')
plt.savefig('confusion_matrix.png')
김개발 씨는 95%라는 높은 정확도에 만족하고 있었습니다. 그런데 팀장님의 날카로운 질문에 식은땀이 났습니다.
사실 자세히 들여다본 적이 없었기 때문입니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.
"클래스 불균형이 있으면 정확도만으로는 부족해. 혼동 행렬부터 확인해봐." 그렇다면 모델 평가는 왜 다양한 지표가 필요할까요?
쉽게 비유하자면, 학교 성적표를 생각해보세요. 평균 90점이라고 해도 국어 100점, 수학 50점일 수 있습니다.
전체 정확도가 높아도 특정 클래스에서 형편없을 수 있습니다. 이런 불균형을 찾아내려면 과목별 점수, 즉 클래스별 지표가 필요합니다.
정확도만 보고 모델을 배포하면 어떤 일이 벌어질까요? 쇼핑몰에서 식품 이미지 분류를 예로 들어봅시다.
전체 정확도 95%라도 '의류' 클래스만 잘 맞추고 '식품'은 60%밖에 못 맞출 수 있습니다. 데이터의 80%가 의류였다면 이런 현상이 나타납니다.
실제 서비스에서 식품 판매자들의 불만이 쏟아지겠죠. 바로 이런 문제를 파악하기 위해 다양한 평가 지표를 사용합니다.
**정밀도(Precision)**는 "모델이 A라고 예측한 것 중 실제 A인 비율"입니다. **재현율(Recall)**은 "실제 A 중에서 모델이 A로 맞춘 비율"입니다.
F1-스코어는 이 둘의 조화평균으로, 불균형한 데이터에서 특히 유용합니다. **혼동 행렬(Confusion Matrix)**은 어떤 클래스를 어떤 클래스로 잘못 분류하는지 한눈에 보여줍니다.
예를 들어 '의류'와 '가방'을 자주 혼동한다면, 이 두 클래스의 학습 데이터를 보강하거나 특징을 더 명확히 구분할 방법을 찾아야 합니다. 위의 코드를 살펴보겠습니다.
**model.eval()**은 모델을 평가 모드로 전환하여 드롭아웃을 비활성화합니다. **torch.no_grad()**는 기울기 계산을 생략하여 메모리를 절약합니다.
classification_report는 클래스별 정밀도, 재현율, F1-스코어를 한 번에 출력해줍니다. 성능이 부족하면 어떻게 개선할까요?
먼저 데이터 측면에서 접근합니다. 성능이 낮은 클래스의 데이터를 추가 수집합니다.
데이터 증강을 강화하거나, 오버샘플링/언더샘플링으로 클래스 균형을 맞춥니다. 다음으로 모델 측면에서 접근합니다.
더 큰 모델을 사용하거나, 하이퍼파라미터를 튜닝합니다. 학습률 스케줄러, 조기 종료, 정규화 기법을 적용합니다.
하지만 주의할 점도 있습니다. 테스트 데이터로 하이퍼파라미터를 튜닝하면 **데이터 누수(Data Leakage)**가 발생합니다.
반드시 검증 데이터를 따로 두고 튜닝해야 합니다. 또한 지나친 튜닝은 과적합을 유발합니다.
K-fold 교차 검증으로 모델의 일반화 성능을 확인하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.
혼동 행렬을 그려보니 '식품'과 '가구' 분류 성능이 낮았습니다. 해당 클래스의 데이터를 보강하고 재학습한 결과, 전체 F1-스코어가 0.92에서 0.96으로 향상되었습니다.
숫자 하나에 현혹되지 마세요. 다양한 관점에서 모델을 평가하고 개선하는 것이 진정한 모델 성능 향상입니다.
실전 팁
💡 - 클래스 불균형이 심하면 weighted F1-score를 주요 지표로 사용하세요
- 혼동 행렬에서 자주 혼동되는 클래스 쌍을 파악하여 데이터 보강에 활용하세요
5. FastAPI로 REST API 구축
모델 개발이 끝나자 팀장님이 새로운 미션을 주었습니다. "이제 이 모델을 API로 만들어서 다른 서비스에서 호출할 수 있게 해줘." 김개발 씨는 Flask를 떠올렸지만, 박시니어 씨가 추천한 건 FastAPI였습니다.
"타입 힌트 지원에, 자동 문서화까지 되거든."
FastAPI는 현대적인 Python 웹 프레임워크로, 빠른 성능과 자동 API 문서화를 제공합니다. 마치 레스토랑에서 주문을 받고 요리를 내놓는 것처럼, API는 이미지를 받아서 분류 결과를 반환합니다.
FastAPI를 사용하면 타입 검증, 에러 처리, 문서화가 자동으로 이루어집니다.
다음 코드를 살펴봅시다.
from fastapi import FastAPI, File, UploadFile, HTTPException
from PIL import Image
import torch
import io
app = FastAPI(title="이미지 분류 API", version="1.0.0")
# 모델과 전처리 함수 로드 (서버 시작 시 한 번만)
model = load_model("models/classifier.pth")
transform = get_transform()
class_names = ['의류', '전자기기', '식품', '가구']
@app.post("/predict")
async def predict_image(file: UploadFile = File(...)):
"""이미지를 받아 카테고리를 분류합니다"""
# 이미지 유효성 검사
if not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="이미지 파일만 업로드 가능합니다")
# 이미지 로드 및 전처리
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
tensor = transform(image).unsqueeze(0)
# 예측 수행
with torch.no_grad():
output = model(tensor)
prob = torch.softmax(output, dim=1)
pred_idx = prob.argmax().item()
return {"category": class_names[pred_idx], "confidence": prob[0][pred_idx].item()}
김개발 씨는 모델은 완성했지만, 다른 서비스에서 어떻게 이 모델을 사용할 수 있을지 고민이었습니다. 매번 Python 스크립트를 실행할 수는 없으니까요.
박시니어 씨가 힌트를 주었습니다. "REST API로 만들면 돼.
HTTP 요청을 보내면 결과를 JSON으로 돌려주는 거지. 프론트엔드든 다른 백엔드든 쉽게 연동할 수 있어." 그렇다면 FastAPI는 무엇이고 왜 선택해야 할까요?
쉽게 비유하자면, FastAPI는 마치 똑똑한 레스토랑 직원과 같습니다. 손님(클라이언트)이 주문(요청)을 하면, 메뉴판에 있는 것인지 확인하고(타입 검증), 주방(모델)에 전달하여 요리(예측)를 가져다줍니다.
게다가 메뉴판(API 문서)도 자동으로 만들어줍니다. Flask나 Django 대신 FastAPI를 선택한 이유가 있습니다.
FastAPI는 비동기(async/await) 처리를 기본 지원합니다. 여러 요청이 동시에 들어와도 효율적으로 처리합니다.
Pydantic을 통한 자동 타입 검증으로 잘못된 입력을 미리 걸러냅니다. 무엇보다 /docs 엔드포인트에서 Swagger UI로 API를 테스트할 수 있어 개발 생산성이 크게 올라갑니다.
위의 코드를 살펴보겠습니다. **@app.post("/predict")**는 POST 요청을 처리하는 엔드포인트를 정의합니다.
UploadFile은 업로드된 파일을 다루는 FastAPI의 특수 타입입니다. HTTPException으로 적절한 에러 응답을 반환합니다.
이미지는 바이트 스트림으로 받아 PIL.Image로 변환합니다. 전처리 후 모델에 입력하고, softmax로 확률을 계산하여 가장 높은 클래스와 신뢰도를 반환합니다.
JSON 형식으로 깔끔하게 응답합니다. 실제 서비스에서는 어떤 추가 기능이 필요할까요?
인증/인가를 추가하여 허가된 사용자만 API를 호출하게 합니다. Rate Limiting으로 과도한 요청을 제한합니다.
로깅으로 요청 기록을 남기고 모니터링으로 서비스 상태를 관찰합니다. 응답 시간이 중요하다면 모델 최적화(ONNX, TensorRT)도 고려해야 합니다.
하지만 주의할 점도 있습니다. 모델 로드를 요청마다 하면 성능이 급격히 떨어집니다.
서버 시작 시 한 번만 로드하고 재사용해야 합니다. 또한 대용량 이미지 업로드에 대비하여 파일 크기 제한을 두어야 합니다.
메모리 누수를 방지하기 위해 이미지 처리 후 명시적으로 메모리를 해제하는 것도 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
FastAPI로 API를 구축한 후 /docs에 접속해보니 깔끔한 문서가 자동 생성되어 있었습니다. 프론트엔드 팀에서도 바로 연동 테스트를 시작할 수 있었습니다.
API는 모델과 세상을 연결하는 다리입니다. FastAPI로 견고하고 문서화된 API를 구축하세요.
실전 팁
💡 - 모델 로드는 전역 변수나 의존성 주입으로 한 번만 수행하세요
- /docs 또는 /redoc 엔드포인트에서 자동 생성된 API 문서를 활용하세요
6. Docker 컨테이너화 및 배포
로컬에서 완벽하게 동작하던 API가 서버에 배포하려니 문제가 생겼습니다. "제 컴퓨터에서는 잘 되는데요..." 김개발 씨의 이 말에 팀원들이 웃었습니다.
박시니어 씨가 말했습니다. "그래서 Docker를 쓰는 거야.
어디서든 똑같이 돌아가게."
Docker는 애플리케이션을 컨테이너로 패키징하여 어떤 환경에서든 동일하게 실행할 수 있게 합니다. 마치 이사할 때 모든 가구를 컨테이너 박스에 넣어 옮기면 새 집에서도 그대로 배치할 수 있는 것처럼, Docker 컨테이너는 코드와 모든 의존성을 함께 패키징합니다.
다음 코드를 살펴봅시다.
# Dockerfile
FROM python:3.10-slim
WORKDIR /app
# 시스템 의존성 설치
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1-mesa-glx libglib2.0-0 && rm -rf /var/lib/apt/lists/*
# Python 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드 복사
COPY . .
# 포트 노출 및 실행
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- ./models:/app/models
environment:
- MODEL_PATH=/app/models/classifier.pth
김개발 씨는 로컬에서 완벽히 동작하는 API를 서버에 배포하려 했습니다. 그런데 Python 버전이 다르다, 라이브러리가 없다, CUDA 버전이 안 맞다...
문제가 한두 가지가 아니었습니다. 박시니어 씨가 다가와 말했습니다.
"그게 바로 환경 의존성 문제야. Docker를 쓰면 깔끔하게 해결돼." 그렇다면 Docker란 정확히 무엇일까요?
쉽게 비유하자면, Docker는 마치 완벽한 이사 박스와 같습니다. 가구(코드)만 옮기는 게 아니라, 콘센트 규격(Python 버전), 수도 배관(라이브러리), 심지어 공기(시스템 환경)까지 통째로 박스에 담아 옮깁니다.
새 집(서버)에서 박스를 열면 모든 것이 그대로 작동합니다. Docker 없이 배포하면 어떤 문제가 생길까요?
"제 컴퓨터에서는 잘 되는데요"라는 말은 개발자들 사이에서 유명한 밈입니다. 로컬과 서버의 환경 차이로 인해 예상치 못한 에러가 발생합니다.
새 팀원이 오면 개발 환경 세팅에 하루 종일 걸립니다. 서버를 이전하면 모든 설정을 처음부터 다시 해야 합니다.
바로 이런 문제를 해결하기 위해 Docker를 사용합니다. Docker의 핵심 개념은 **이미지(Image)**와 **컨테이너(Container)**입니다.
이미지는 애플리케이션의 스냅샷이고, 컨테이너는 이미지를 실행한 인스턴스입니다. Dockerfile은 이미지를 만드는 레시피이고, docker-compose는 여러 컨테이너를 조율하는 지휘자입니다.
위의 Dockerfile을 살펴보겠습니다. FROM python:3.10-slim은 기반 이미지를 지정합니다.
slim 버전을 사용하여 이미지 크기를 줄입니다. WORKDIR /app은 작업 디렉토리를 설정합니다.
RUN은 명령어를 실행하고, COPY는 파일을 복사합니다. CMD는 컨테이너 시작 시 실행할 명령입니다.
docker-compose.yml은 서비스 구성을 정의합니다. volumes로 모델 파일을 외부에서 마운트하면 이미지를 다시 빌드하지 않고도 모델을 교체할 수 있습니다.
environment로 환경 변수를 주입합니다. 실제 배포는 어떻게 진행할까요?
먼저 **docker build -t image-classifier .**로 이미지를 빌드합니다. docker run -p 8000:8000 image-classifier로 컨테이너를 실행합니다.
AWS ECS, Google Cloud Run, Kubernetes 같은 컨테이너 오케스트레이션 서비스를 사용하면 자동 스케일링과 무중단 배포도 가능합니다. 하지만 주의할 점도 있습니다.
Docker 이미지 크기가 너무 크면 배포 시간이 오래 걸립니다. 멀티스테이지 빌드를 활용하고, 불필요한 파일은 .dockerignore로 제외하세요.
GPU를 사용하려면 NVIDIA Container Toolkit을 설치하고 --gpus all 옵션으로 실행해야 합니다. 또한 모델 파일은 이미지에 포함시키지 말고 볼륨으로 마운트하는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. Docker로 컨테이너화한 후 배포가 한결 쉬워졌습니다.
서버에서도, 동료의 컴퓨터에서도, 심지어 고객사 환경에서도 동일하게 동작했습니다. "이제 '제 컴퓨터에서는 잘 되는데요'라는 말은 안 해도 되겠네요!" Docker는 현대 DevOps의 필수 도구입니다.
한 번 배워두면 어떤 프로젝트에서든 활용할 수 있습니다. 여러분의 이미지 분류 서비스도 Docker로 세상에 내보내세요.
실전 팁
💡 - .dockerignore 파일을 작성하여 불필요한 파일(가상환경, 로그, 캐시)을 이미지에서 제외하세요
- 모델 파일은 이미지에 굽지 말고 볼륨 마운트나 S3 같은 외부 스토리지에서 로드하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (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, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.