2026/04/12

김기훈·4일 전

TIL

목록 보기
188/191

도커

docker-compose.yml

안좋은 방법

version: '3.8' # 도커 컴포즈 설정 파일의 버전을 명시합니다.

services: # 이 프로젝트에서 실행할 서비스(컨테이너)들을 정의합니다.
  db: # 데이터베이스 서비스를 정의합니다.
    image: postgres:15 # 포스트그레SQL 15 버전의 공식 이미지를 가져와 사용합니다.
    volumes: # 컨테이너가 삭제되어도 데이터가 사라지지 않도록 내 컴퓨터와 저장 공간을 연결합니다.
      - postgres_data:/var/lib/postgresql/data/
    environment: # 데이터베이스 접속에 필요한 환경 변수들을 설정합니다.
      - POSTGRES_DB=promise_db # 사용할 데이터베이스의 이름을 지정합니다.
      - POSTGRES_USER=postgres # 데이터베이스 관리자 계정 이름을 설정합니다.
      - POSTGRES_PASSWORD=postgres # 관리자 계정의 비밀번호를 설정합니다.

  web: # 장고 웹 서버 서비스를 정의합니다.
    build: . # 현재 폴더에 있는 Dockerfile을 사용하여 이미지를 빌드합니다.
    command: python manage.py runserver 0.0.0.0:8000 # 컨테이너가 켜질 때 실행할 장고 서버 시작 명령어입니다.
    volumes: # 내 컴퓨터의 소스 코드 변경 사항이 실시간으로 컨테이너에 반영되도록 연결합니다.
      - .:/app
    ports: # 내 컴퓨터의 8000번 포트와 컨테이너의 8000번 포트를 연결합니다.
      - "8000:8000"
    environment: # 장고 서버가 데이터베이스에 접속할 수 있도록 정보를 전달합니다.
      - DATABASE_URL=postgres://postgres:postgres@db:5432/promise_db
    depends_on: # 데이터베이스 컨테이너가 먼저 실행된 후에 웹 서버가 켜지도록 순서를 보장합니다.
      - db

volumes: # 서비스들에서 공통으로 사용할 저장 공간(볼륨)을 선언합니다.
  postgres_data:
  • 이유

    • 비밀번호가 파일에 그대로 노출되어 있음

개선 방법

  • env 파일 활용
    • 데이터베이스 비밀번호 같은 민감한 정보를 .env 파일로 분리하여 관리
  • container_name을 직접 지정
    • 터미널에서 실행 중인 도커를 확인할 때 어떤 컨테이너인지 한눈에 알아볼 수 있음
  • networks를 설정
    • 앱의 컨테이너들끼리만 안전하게 통신할 수 있는 전용망을 구축
  • 자동화(command 명령어)
    • 컨테이너가 켜질 때 python manage.py migrate를 먼저 실행하도록 설정

설정

  • .env파일 생성

  • docker-compose.yml 작성

    • 도커 컴포즈 안에서 ${변수명} 형태를 사용하면 .env 파일에 있는 값을 동적으로 불러옴
version: '3.8'

services:
  # ---------------------------------------------------------------------------
  # 1. Django 백엔드 웹 서버
  # ---------------------------------------------------------------------------
  backend:
    build: .
    container_name: promise_backend
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    env_file:
      - .env # 만든 .env 파일을 읽어옵니다.
    environment:
      - POSTGRES_HOST=promise_db # 아래 db 서비스의 컨테이너 이름과 똑같이 맞춰줍니다.
    depends_on:
      - db
    networks:
      - app_network
    command: >
      sh -c "python manage.py migrate &&
             python manage.py runserver 0.0.0.0:8000"

  # ---------------------------------------------------------------------------
  # 2. PostgreSQL 데이터베이스
  # ---------------------------------------------------------------------------
  db:
    image: postgres:15
    container_name: promise_db 
    environment:
      - POSTGRES_DB=${POSTGRES_DB} # .env 파일에서 POSTGRES_DB 값을 가져와 꽂아줍니다.
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app_network

volumes:
  postgres_data: # 데이터베이스 정보가 날아가지 않도록 저장 공간을 확보합니다.

networks:
  app_network:
    driver: bridge # 컨테이너들끼리 통신할 수 있는 가상의 다리를 놓아줍니다.
  • Django 설정(settings.py) 수정

    • 파이썬의 내장 모듈인 os를 사용하면
      • 현재 프로그램이 실행되고 있는 운영체제의 환경 변수(os.environ)에 접근할 수 있음
    • 도커 컴포즈가 .env의 내용들을 컨테이너 환경 변수로 밀어 넣어 주었기 때문에
      • 장고는 텍스트 파일의 비밀번호를 직접 읽는 대신 운영체제에 설정된 값을 안전하게 가져와 사용하는 원리
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

——————————————————————————————————————[비교]—————————————————————————————————————————
import os

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('POSTGRES_DB', 'promise_db'), # 운영체제 환경변수에서 값을 꺼내옵니다.
        'USER': os.environ.get('POSTGRES_USER', 'postgres'),
        'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'your_secret_password'),
        'HOST': os.environ.get('POSTGRES_HOST', 'promise_db'), # docker-compose에서 설정한 HOST를 바라봅니다.
        'PORT': '5432',
    }
}

서버 실행

  • docker compose up --build


문제발생

2026-04-12 14:40:40.554 | Traceback (most recent call last):
2026-04-12 14:40:40.554 |   File "/usr/local/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 25, in <module>
2026-04-12 14:40:40.554 |     import psycopg as Database
2026-04-12 14:40:40.554 | ModuleNotFoundError: No module named 'psycopg'
2026-04-12 14:40:40.554 | 
2026-04-12 14:40:40.554 | During handling of the above exception, another exception occurred:
2026-04-12 14:40:40.554 | 
2026-04-12 14:40:40.554 | Traceback (most recent call last):
2026-04-12 14:40:40.554 |   File "/usr/local/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 27, in <module>
2026-04-12 14:40:40.554 |     import psycopg2 as Database
2026-04-12 14:40:40.554 | ModuleNotFoundError: No module named 'psycopg2'
2026-04-12 14:40:40.554 | 
2026-04-12 14:40:40.554 | During handling of the above exception, another exception occurred:
2026-04-12 14:40:40.554 | 
2026-04-12 14:40:40.554 | Traceback (most recent call last):
2026-04-12 14:40:40.555 |   File "/app/manage.py", line 23, in <module>
2026-04-12 14:40:40.555 |     main()
2026-04-12 14:40:40.555 |     ~~~~^^
2026-04-12 14:40:40.555 |   File "/app/manage.py", line 19, in main
2026-04-12 14:40:40.555 |     execute_from_command_line(sys.argv)
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/core/management/__init__.py", line 443, in execute_from_command_line
2026-04-12 14:40:40.555 |     utility.execute()
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~~~^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/core/management/__init__.py", line 417, in execute
2026-04-12 14:40:40.555 |     django.setup()
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/__init__.py", line 24, in setup
2026-04-12 14:40:40.555 |     apps.populate(settings.INSTALLED_APPS)
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/apps/registry.py", line 116, in populate
2026-04-12 14:40:40.555 |     app_config.import_models()
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~~~~~~~~~~~~^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/apps/config.py", line 269, in import_models
2026-04-12 14:40:40.555 |     self.models_module = import_module(models_module_name)
2026-04-12 14:40:40.555 |                          ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/importlib/__init__.py", line 88, in import_module
2026-04-12 14:40:40.555 |     return _bootstrap._gcd_import(name[level:], package, level)
2026-04-12 14:40:40.555 |            ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap>", line 1395, in _gcd_import
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap_external>", line 1023, in exec_module
2026-04-12 14:40:40.555 |   File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/contrib/auth/models.py", line 5, in <module>
2026-04-12 14:40:40.555 |     from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/contrib/auth/base_user.py", line 43, in <module>
2026-04-12 14:40:40.555 |     class AbstractBaseUser(models.Model):
2026-04-12 14:40:40.555 |     ...<120 lines>...
2026-04-12 14:40:40.555 |             )
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/models/base.py", line 145, in __new__
2026-04-12 14:40:40.555 |     new_class.add_to_class("_meta", Options(meta, app_label))
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/models/base.py", line 393, in add_to_class
2026-04-12 14:40:40.555 |     value.contribute_to_class(cls, name)
2026-04-12 14:40:40.555 |     ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/models/options.py", line 238, in contribute_to_class
2026-04-12 14:40:40.555 |     self.db_table, connection.ops.max_name_length()
2026-04-12 14:40:40.555 |                    ^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/utils/connection.py", line 15, in __getattr__
2026-04-12 14:40:40.555 |     return getattr(self._connections[self._alias], item)
2026-04-12 14:40:40.555 |                    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/utils/connection.py", line 62, in __getitem__
2026-04-12 14:40:40.555 |     conn = self.create_connection(alias)
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/utils.py", line 196, in create_connection
2026-04-12 14:40:40.555 |     backend = load_backend(db["ENGINE"])
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/utils.py", line 116, in load_backend
2026-04-12 14:40:40.555 |     return import_module("%s.base" % backend_name)
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/importlib/__init__.py", line 88, in import_module
2026-04-12 14:40:40.555 |     return _bootstrap._gcd_import(name[level:], package, level)
2026-04-12 14:40:40.555 |            ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2026-04-12 14:40:40.555 |   File "/usr/local/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 29, in <module>
2026-04-12 14:40:40.555 |     raise ImproperlyConfigured("Error loading psycopg2 or psycopg module")
2026-04-12 14:40:40.555 | django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
  • 원인

    • ModuleNotFoundError: No module named 'psycopg2'
    • 장고 웹 서버가 데이터베이스(PostgreSQL)와 데이터를 주고받으려면
    • 중간에서 통역을 해줄 파이썬 전용 통신 패키지가 필요함
    • 현재 도커 컨테이너 내부에 이 패키지가 설치되지 않아서 발생한 문제
  • 해결

    • poetry add psycopg2-binary

스웨거

drf-spectacular

패키지 설치

  • poetry add drf-spectacular

settings.py 수정

  • REST_FRAMEWORK 설정에서
    • 스키마(데이터의 구조와 규칙) 생성 담당자를 drf_spectacular로 교체함으로써
    • 앞으로 작성할 모든 데이터 모델과 뷰의 모양새가 OpenAPI 표준에 맞추어 번역되게 됨
THIRD_PARTY_APPS: list[str] = []  # 외부에서 설치한 라이브러리 앱들입니다.

——————————————————————————————————————[비교]—————————————————————————————————————————
THIRD_PARTY_APPS: list[str] = [
    'rest_framework',
    'drf_spectacular',
]  # 외부에서 설치한 라이브러리 앱들입니다.

# 장고 레스트 프레임워크의 전역적인 작동 방식을 세밀하게 제어하는 설정 딕셔너리
REST_FRAMEWORK = { 
    # API 구조를 분석할 때 기본 도구 대신 스펙타큘러의 꼼꼼한 분석 도구를 최우선으로 사용하도록 강제
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 
} 

# 스웨거 웹 페이지 화면 상단에 노출될 프로젝트의 간판 정보들을 설정하는 딕셔너리
SPECTACULAR_SETTINGS = { 
    # 완성된 스웨거 문서의 가장 눈에 띄는 곳에 표시될 공식 서비스 이름을 지정
    'TITLE': 'Promise App API', 
    'DESCRIPTION': '약속 잡기 어플리케이션을 위한 백엔드 API 명세서입니다.',
    # 현재 API의 버전을 숫자로 명시하여 나중에 기능이 크게 변경되었을 때 헷갈리지 않도록 돕기
    'VERSION': '1.0.0', 
} 

urls.py

from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [ 
    path('admin/', admin.site.urls), 
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
] 

결과


profile
안녕하세요.

0개의 댓글