장고 실전 #6 테스트와 배포 — Docker, gunicorn, nginx
장고 실전 시리즈의 마지막 — 테스트와 배포 입니다. #1 ~ #5에서 만든 API가 정말 동작하는지 자동으로 검증하고, 한 명령으로 컨테이너 빌드해 프로덕션에 올리는 흐름까지 한곳에 정리합니다.
DRF 테스트 — APIClient와 APITestCase
#
Django의 Client (중급 #7 테스트)는 폼 인코딩이 기본이라 JSON API 테스트에는 어색합니다. DRF의 **APIClient**가 JSON을 기본으로 다루고, 인증 helper도 같이 줍니다.
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase
User = get_user_model()
class PostAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username="alice", password="pw")
self.client.force_authenticate(user=self.user)
def test_create_post(self):
response = self.client.post(
"/api/posts/",
{"title": "첫 글", "body": "본문 내용"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["title"], "첫 글")
self.assertEqual(response.data["author"], self.user.id)
def test_list_posts(self):
response = self.client.get("/api/posts/")
self.assertEqual(response.status_code, 200)
self.assertIn("results", response.data) # 페이지네이션 envelopeAPITestCase의 쓰임
#
APITestCase는 Django의 TestCase를 상속하면서 self.client를 APIClient로 바꿔줍니다. DB 트랜잭션 자동 롤백도 그대로.
force_authenticate — 인증 우회
#
테스트마다 토큰을 만들고 헤더를 붙이는 건 번거롭습니다. force_authenticate(user=...) 한 줄로 인증 상태를 강제합니다. 인증 미들웨어를 건너뛰고 request.user만 주입하는 방식입니다.
def test_with_real_token(self):
response = self.client.post("/api/auth/token/",
{"username": "alice", "password": "pw"}, format="json")
token = response.data["access"]
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
# 이후 모든 요청에 헤더 자동 추가force_authenticate가 빠르고, credentials는 인증 흐름 자체를 검증할 때 (로그인 테스트 등).
pytest-django — 더 모던한 흐름 #
unittest 기반의 APITestCase도 충분하지만, pytest가 더 가볍고 fixture가 강력합니다.
uv add --dev pytest pytest-django pytest-cov[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "mysite.settings"
python_files = ["test_*.py", "*_test.py"]
addopts = "--strict-markers --reuse-db"--reuse-db 옵션이 핵심 — 매번 마이그레이션을 다시 적용하지 않아 테스트 시작이 빨라집니다. 마이그레이션 자체를 바꾼 다음에는 --create-db로 한 번 다시 만들면 됩니다.
함수 스타일 테스트 #
import pytest
from rest_framework.test import APIClient
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture
def user(db):
return User.objects.create_user(username="alice", password="pw")
@pytest.fixture
def auth_client(user):
client = APIClient()
client.force_authenticate(user=user)
return client
@pytest.mark.django_db
def test_create_post(auth_client, user):
response = auth_client.post(
"/api/posts/",
{"title": "첫 글", "body": "본문"},
format="json",
)
assert response.status_code == 201
assert response.data["author"] == user.id
@pytest.mark.django_db
def test_unauthenticated_blocked():
client = APIClient()
response = client.post("/api/posts/", {"title": "x", "body": "y"}, format="json")
assert response.status_code == 401@pytest.mark.django_db가 DB 접근을 허용. fixture가 의존성 주입처럼 동작 — 같은 fixture가 여러 테스트에 자동 공유.
factory_boy — fixture 데이터 #
매 테스트마다 같은 모델을 직접 만드는 건 지저분해집니다. factory_boy가 그 일을 합니다.
uv add --dev factory-boyimport factory
from django.contrib.auth import get_user_model
from blog.models import Post, Comment
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
username = factory.Sequence(lambda n: f"user{n}")
email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
password = factory.PostGenerationMethodCall("set_password", "pw")
class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post
author = factory.SubFactory(UserFactory)
title = factory.Faker("sentence", nb_words=4)
body = factory.Faker("paragraph", nb_sentences=5)
published = True
class CommentFactory(factory.django.DjangoModelFactory):
class Meta:
model = Comment
post = factory.SubFactory(PostFactory)
author = factory.SubFactory(UserFactory)
body = factory.Faker("sentence")@pytest.mark.django_db
def test_post_with_comments(auth_client):
post = PostFactory()
CommentFactory.create_batch(3, post=post)
response = auth_client.get(f"/api/posts/{post.id}/")
assert response.status_code == 200Sequence— 자동 증가 카운터로 unique 값LazyAttribute— 다른 필드 기반 동적 값SubFactory— FK 자동 생성 (없으면 새로 만듦)Faker— 가짜 텍스트/이름/이메일create_batch(N)— N 건 한 번에
테스트 데이터 셋업 코드가 선언적으로 짧아집니다.
응답 검증 — status / body / headers #
@pytest.mark.django_db
def test_post_detail_shape(auth_client):
post = PostFactory(title="검증용")
response = auth_client.get(f"/api/posts/{post.id}/")
# status
assert response.status_code == 200
# body
data = response.data
assert data["id"] == post.id
assert data["title"] == "검증용"
assert "author" in data
assert "created_at" in data
# 의도치 않은 필드 노출 방지
assert "secret_field" not in data
# headers
assert response["Content-Type"].startswith("application/json")API 테스트는 응답 구조 검증이 핵심입니다. 필드 이름, 타입, 누락된 것, 의도치 않게 노출된 것 — 클라이언트와의 contract가 깨졌는지 잡아냅니다.
흐름 테스트 — 시나리오 #
여러 요청이 엮인 시나리오:
@pytest.mark.django_db
def test_full_post_lifecycle(auth_client, user):
# Create
create = auth_client.post("/api/posts/",
{"title": "lifecycle", "body": "x"}, format="json")
assert create.status_code == 201
post_id = create.data["id"]
# Read
read = auth_client.get(f"/api/posts/{post_id}/")
assert read.data["published"] is False
# Publish action
publish = auth_client.post(f"/api/posts/{post_id}/publish/")
assert publish.status_code == 200
read2 = auth_client.get(f"/api/posts/{post_id}/")
assert read2.data["published"] is True
# Delete
delete = auth_client.delete(f"/api/posts/{post_id}/")
assert delete.status_code == 204
# Verify
final = auth_client.get(f"/api/posts/{post_id}/")
assert final.status_code == 404권한 테스트 — 다른 사용자 #
@pytest.mark.django_db
def test_other_user_cannot_edit():
owner = UserFactory()
other = UserFactory()
post = PostFactory(author=owner)
client = APIClient()
client.force_authenticate(user=other)
response = client.patch(f"/api/posts/{post.id}/",
{"title": "악의적 수정"}, format="json")
assert response.status_code == 403#2의 IsOwnerOrReadOnly가 잘 동작하는지 검증하는 회귀 테스트.
Celery task 테스트 #
#4의 task도 테스트가 필요합니다. 두 가지 모드.
1) Eager 모드 — 동기로 실행 #
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = Truetask.delay()가 호출되는 즉시 동기적으로 실행됩니다. 테스트에서 task가 실제로 실행되는지 검증하기 좋습니다.
2) Mocking #
from unittest.mock import patch
@pytest.mark.django_db
def test_publish_triggers_notification(auth_client):
post = PostFactory(author=auth_client.handler._force_user)
with patch("blog.views.notify_subscribers.delay") as mock_task:
response = auth_client.post(f"/api/posts/{post.id}/publish/")
assert response.status_code == 200
mock_task.assert_called_once_with(post.id)task의 내부 동작이 아니라 호출 자체만 검증할 때.
Schema 회귀 테스트 #
#5에서 본 schema diff를 테스트로.
import json
from django.core.management import call_command
from io import StringIO
def test_schema_unchanged():
out = StringIO()
call_command("spectacular", "--validate", stdout=out)
current = json.loads(out.getvalue() or "{}")
with open("schema.snapshot.json") as f:
snapshot = json.load(f)
assert current == snapshot, "API schema가 바뀌었습니다 — snapshot 갱신 필요"이런 테스트가 있으면 무심코 breaking change를 머지하기 어렵습니다. 의도된 변경이라면 snapshot을 업데이트하는 별도 PR로.
커버리지 #
uv run pytest --cov=blog --cov-report=term-missingName Stmts Miss Cover Missing
--------------------------------------------------------
blog/models.py 42 0 100%
blog/serializers.py 28 2 93% 45-46
blog/views.py 67 4 94% 89-92
blog/permissions.py 18 0 100%
blog/tasks.py 23 3 87% 34-36
--------------------------------------------------------
TOTAL 178 9 95%100% 가 목표가 아니라 중요 경로가 검증되는지가 목표. API는 통합 테스트 비중이 높아 80~90% 가 자연스럽게 나오는 범위입니다.
여기까지가 테스트 파트입니다. 이제 배포 파트로 넘어갑니다.
배포 — Django + DRF의 표준 스택 #
운영 환경에서는 manage.py runserver를 쓰면 안 됩니다. 표준 조합은:
| 레이어 | 도구 | 쓰임 |
|---|---|---|
| WSGI 서버 | gunicorn (또는 uWSGI) | Python 앱 실행 |
| 리버스 프록시 | nginx (또는 Caddy, Traefik) | TLS 종료, 정적 파일, 로드 밸런싱 |
| 컨테이너 | Docker | 환경 일관성 |
| DB | PostgreSQL | 본 데이터 |
| 캐시 / 큐 | Redis | 캐시 + Celery 브로커 |
| 워커 | Celery worker / beat | 백그라운드 작업 (#4) |
고급 #7 배포 보안, 중급 #6 Static/Media 운영의 패턴이 그대로 들어옵니다.
Dockerfile — 멀티스테이지 빌드 #
# === 1) 빌드 스테이지 ===
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
# 의존성만 먼저 (캐시 효율)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# 코드
COPY . .
# 정적 파일 수집 (DEBUG=False로 collectstatic)
ENV DJANGO_SETTINGS_MODULE=mysite.settings
ENV SECRET_KEY=build-time-dummy
RUN uv run python manage.py collectstatic --noinput
# === 2) 런타임 스테이지 ===
FROM python:3.13-slim AS runtime
# 시스템 패키지 (psycopg 같은 C 의존성)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# 비루트 사용자
RUN useradd --create-home --uid 1000 appuser
WORKDIR /app
# 빌더에서 venv + 코드 + 정적 파일 복사
COPY --from=builder --chown=appuser:appuser /app /app
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
CMD ["gunicorn", "mysite.wsgi", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--access-logfile", "-", \
"--error-logfile", "-"]핵심 패턴:
- 멀티스테이지 — 빌드 도구는 최종 이미지에서 빠짐
- 의존성 먼저, 코드 나중 — 코드만 바뀌면 의존성 레이어 캐시 재사용
- 비루트 사용자 — 보안
--frozen --no-dev—uv.lock그대로, 개발 의존성 제외collectstatic— 정적 파일을 한 곳에 모음 (중급 #6)
.dockerignore
#
.venv/
__pycache__/
*.pyc
.pytest_cache/
.git/
.github/
*.md
.env
.env.*
db.sqlite3
media/
node_modules/이미지에 들어가지 않아도 되는 것들. 빌드 속도 + 보안.
gunicorn — WSGI 서버 #
gunicorn mysite.wsgi --bind 0.0.0.0:8000 \
--workers 4 \ # CPU 코어 수 × 2 + 1 정도
--worker-class sync \ # 기본 (또는 gthread, gevent)
--timeout 30 \ # 워커 타임아웃
--max-requests 1000 \ # 워커 재시작 (메모리 누수 방지)
--max-requests-jitter 50 \ # 동시 재시작 방지
--access-logfile - \
--error-logfile -워커 수 정하기 #
| 트래픽 | 권장 |
|---|---|
| 작은 트래픽 / 단일 머신 | 2 × CPU + 1 |
| 외부 IO 많음 | gthread worker, --threads 4-8 |
| 비동기 view 많음 | uvicorn (ASGI)로 전환 — --worker-class uvicorn.workers.UvicornWorker |
고급 #1 Async views가 많으면 ASGI (uvicorn)로 가는 게 자연스럽습니다.
nginx — 리버스 프록시 #
nginx가 앞단에서 받고, gunicorn으로 프록시합니다. 분담은 이렇습니다:
| 레이어 | 담당 |
|---|---|
| nginx | TLS 종료, 정적 파일 서빙, gzip, rate limit, 헤더 조정 |
| gunicorn | Django/DRF 실행 |
upstream django {
server web:8000;
}
server {
listen 80;
server_name api.example.com;
# HTTPS 리다이렉트는 보통 클라우드 LB가 처리
# 직접 운영하면 letsencrypt + listen 443 ssl
client_max_body_size 10M;
# 정적 파일 — Django가 아닌 nginx가 직접
location /static/ {
alias /app/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /app/media/;
expires 7d;
}
# API는 gunicorn으로 프록시
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
}
}X-Forwarded-Proto와 Django
#
nginx가 HTTPS 종료를 했지만 gunicorn은 HTTP로 받습니다. Django가 그래도 “이 요청은 HTTPS” 라고 인식하게 하려면.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
ALLOWED_HOSTS = ["api.example.com"]고급 #7 배포 보안의 헤더 설정과 같이 가야 합니다.
docker-compose — 풀스택 한 번에 #
services:
web:
build: .
command: >
sh -c "python manage.py migrate &&
gunicorn mysite.wsgi --bind 0.0.0.0:8000 --workers 4
--access-logfile - --error-logfile -"
environment:
DJANGO_SETTINGS_MODULE: mysite.settings
SECRET_KEY: ${SECRET_KEY}
DATABASE_URL: postgresql://blog:blog@db:5432/blog
REDIS_URL: redis://redis:6379/0
ALLOWED_HOSTS: api.example.com,localhost
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- static-files:/app/staticfiles
- media-files:/app/media
expose: ["8000"]
worker:
build: .
command: celery -A mysite worker -l info --concurrency=4
environment:
DJANGO_SETTINGS_MODULE: mysite.settings
SECRET_KEY: ${SECRET_KEY}
DATABASE_URL: postgresql://blog:blog@db:5432/blog
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
beat:
build: .
command: celery -A mysite beat -l info
environment:
DJANGO_SETTINGS_MODULE: mysite.settings
SECRET_KEY: ${SECRET_KEY}
DATABASE_URL: postgresql://blog:blog@db:5432/blog
REDIS_URL: redis://redis:6379/0
depends_on: [db, redis]
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static-files:/app/staticfiles:ro
- media-files:/app/media:ro
ports: ["80:80"]
depends_on: [web]
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: blog
POSTGRES_USER: blog
POSTGRES_PASSWORD: blog
healthcheck:
test: ["CMD-SHELL", "pg_isready -U blog"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- pg-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
pg-data:
redis-data:
static-files:
media-files:docker compose up -d한 명령으로 web + worker + beat + nginx + db + redis가 같이 뜹니다. nginx가 정적 파일 볼륨을 web과 공유 — Django의 collectstatic 결과를 nginx가 직접 서빙.
환경 변수 — django-environ
#
코드에 비밀을 박지 않기 위해 환경 변수로.
uv add django-environimport environ
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
)
environ.Env.read_env()
SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
DATABASES = {"default": env.db()} # DATABASE_URL=postgres://...
CACHES = {"default": env.cache()} # REDIS_URL=redis://...
CELERY_BROKER_URL = env("REDIS_URL")
CELERY_RESULT_BACKEND = env("REDIS_URL").env 파일은 git에서 제외, 프로덕션은 시크릿 매니저 (AWS Secrets Manager / GCP Secret Manager / Doppler 등) 또는 플랫폼 환경 변수.
마이그레이션 — 배포 시점 자동화 #
가장 흔한 두 패턴.
옵션 1 — 컨테이너 시작 시 적용 #
#!/usr/bin/env bash
set -e
# DB가 ready 될 때까지 대기 (compose의 healthcheck와 함께)
python manage.py migrate --noinput
# 정적 파일 (이미 빌드 시점에 했지만 갱신용)
python manage.py collectstatic --noinput
exec "$@"COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "mysite.wsgi", "--bind", "0.0.0.0:8000", "--workers", "4"]작은 서비스는 이 패턴이 단순합니다. 단점 — 여러 인스턴스가 동시에 마이그레이션을 시도할 수 있음.
옵션 2 — CI/CD 단계에서 분리 #
- name: Run migrations
run: |
docker run --rm \
-e DATABASE_URL=${{ secrets.DATABASE_URL }} \
${{ env.IMAGE }} \
python manage.py migrate --noinput
- name: Deploy
run: |
# 새 이미지로 web/worker 롤링 업데이트큰 서비스는 옵션 2가 안전합니다 — 마이그레이션이 한 번만 실행되고, 실패 시 배포가 중단.
헬스체크 #
LB / 오케스트레이터가 컨테이너 상태를 알아야 합니다.
from django.db import connection
from django.http import JsonResponse
def health(request):
return JsonResponse({"status": "ok"})
def ready(request):
"""DB / Redis까지 정상이어야 ready."""
try:
with connection.cursor() as cur:
cur.execute("SELECT 1")
except Exception as exc:
return JsonResponse({"status": "db_down", "error": str(exc)}, status=503)
return JsonResponse({"status": "ready"})urlpatterns = [
path("health/", health),
path("ready/", ready),
...
]/health/— liveness — 프로세스가 살아있는가 (실패 시 재시작)/ready/— readiness — 트래픽 받을 준비가 됐는가 (실패 시 LB에서 제외)
쿠버네티스 / Fargate / 클라우드 LB가 이 두 엔드포인트를 정기적으로 호출.
배포 대상 — 가벼운 옵션들 #
| 쓰임 | |
|---|---|
| Railway | 가장 가벼움. GitHub 연결 → 자동 배포. Postgres/Redis 자동 프로비저닝. 빠른 프로토타입에 최적. |
| Fly.io | 글로벌 분산. flyctl CLI로 배포. fly.toml 한 파일. |
| Render | Heroku 대체. render.yaml으로 web/worker/cron 정의. |
| AWS ECS Fargate | 큰 회사 표준. ECR + ECS + ALB + RDS + ElastiCache. 운영 부담 있음. |
| GCP Cloud Run | 컨테이너 서버리스. 트래픽 0 일 때 비용 0. 큰 메모리/긴 작업에는 부적합. |
| 자체 VPS | DigitalOcean / Hetzner. Docker Compose 한 번에 정리합니다. 가장 저렴, 운영 부담 큼. |
작은 사이드 프로젝트는 Railway / Fly, 회사 서비스는 AWS / GCP가 흔한 선택입니다.
프로덕션 체크리스트 #
배포 전 확인 — 고급 #7의 보안 체크리스트와 같이.
-
DEBUG=False -
SECRET_KEY— 환경 변수, 64+ 바이트 랜덤 -
ALLOWED_HOSTS— 명시적 도메인 (와일드카드 X) - HTTPS 강제 (
SECURE_SSL_REDIRECT, HSTS 헤더) - CORS — 명시적 origin (
*X) - DB 비밀번호 — 강한 랜덤
- JWT
SIGNING_KEY— 별도 강한 키 - 로그 레벨 — INFO 이상
- Sentry — 에러 추적
- 헬스체크 —
/health,/ready - 마이그레이션 — 자동화
- 백업 — DB 자동 백업 활성
- 모니터링 — APM (DataDog / New Relic / Grafana)
- rate limiting — DRF throttling + nginx
- 정적 파일 — nginx 또는 CDN
- 미디어 파일 — S3 / GCS (django-storages)
이 체크리스트는 시작점이고, 트래픽이 커지면 더 추가됩니다.
시리즈 회고 #
6편을 거쳐 장고 실전 — DRF의 흐름이 완성됐습니다.
| # | 다룬 것 |
|---|---|
| 1 | DRF 시작 — Serializer, ModelSerializer, ViewSet, Router |
| 2 | 인증/권한 — Token, JWT (simplejwt), permission classes, IsOwner |
| 3 | Filtering / Ordering / Pagination (PageNumber/LimitOffset/Cursor) |
| 4 | Celery — task, retry, Beat, Flower, transaction.on_commit |
| 5 | drf-spectacular — OpenAPI, Swagger UI, schema 회귀 테스트 |
| 6 | 테스트 (APITestCase, pytest-django, factory_boy) + Docker/gunicorn/nginx |
블로그 API 한 도메인을 잡고 점층적으로 쌓아 — 모델 → API → 인증 → 필터/페이지네이션 → 비동기 → 문서 → 테스트/배포 — 가 한곳에 모였습니다.
장고 트랙 회고 (4 시리즈 / 27편) #
| 시리즈 | 편수 | 핵심 |
|---|---|---|
| 장고 기초 | 7 | 셋업, ORM, URL/View, Template, Form, Admin/Auth |
| 장고 중급 | 7 | CBV, ORM 중급, Signals, 사용자/권한, 메시지/세션, Static, 테스트 |
| 장고 고급 | 7 | Async/ASGI, 커스텀 명령, 쿼리 최적화, 캐싱, 트랜잭션, Channels, 배포 보안 |
| 장고 실전 (DRF) | 6 | DRF 풀스택 |
이 트랙을 처음 기초 #1 Django 란으로 돌아가서 보면, 처음 봤을 때와 다른 관점에서 같은 글이 다시 읽힙니다 — 한 모델을 만드는 순간 그 뒤에 ORM 최적화, 캐시, 시그널, async, 배포까지 이어지는 흐름이 보이게 됩니다. 그게 한 트랙을 끝까지 따라간 이득입니다.
다음 학습 방향 #
- Django Channels 깊이 (고급 #6에서 시작) — WebSocket, 실시간 알림, presence
- DRF + GraphQL — Strawberry / Graphene으로 GraphQL 게이트웨이
- 마이크로서비스 vs 모놀리식 — Django 모놀리식 + 일부 FastAPI 마이크로서비스 (FastAPI 트랙과 비교)
- DataOps / ML 통합 — Celery + scikit-learn / LangChain
- 퍼포먼스 — Django 프로파일링, Postgres 깊이, 분산 캐시
- 운영 — Kubernetes 배포, observability (OpenTelemetry), GitOps
각 방향은 별도 트랙으로 다룰 가치가 있는 영역들입니다.
긴 트랙을 따라와 주셔서 고맙습니다. 코드를 직접 쳐 보면서 이 시리즈의 패턴이 자기 프로젝트에 어떻게 들어맞는지 한 번 옮겨 보면, 글로 읽은 것이 손에 붙는 순간이 옵니다 — 그 지점에서 만나뵙겠습니다.