반응형

Python을 사용하다 보면 반복되는 값이나 의미 있는 고정 값들을 다루게 된다. 이때 Enum을 사용하면 코드의 가독성, 안정성, 그리고 유지보수성을 크게 향상시킬 수 있다. 이 글에서는 Python 공식 문서를 바탕으로 Enum의 기본 개념부터 고급 기능, 그리고 실무에서 어떻게 활용할 수 있는지까지 정리해 보았다.



Enum이란?

EnumEnumeration(열거형)의 줄임말로, 이름이 붙은 상수들의 집합이다. 예를 들어 요일이나 상태값처럼 제한된 선택지를 다룰 때 사용하면 좋다.

from enum import Enum

class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3

왜 Enum을 사용할까?

  • 의미 있는 이름으로 값을 표현할 수 있음 (1Weekday.MONDAY)
  • 잘못된 값 사용 방지 (Weekday("FRIDAY") 같은 잘못된 값 차단)
  • 자동완성, 타입 안정성, 가독성 향상

기본 사용법

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color.RED.name)   # 'RED'
print(Color.RED.value)  # 1
  • Enum.name: 이름 (RED)
  • Enum.value: 값 (1)

다중 상속 (str, int)

파이썬에서는 str, intEnum을 함께 상속해 혼합형 Enum(Mixin Enum)을 만들 수 있다.

class Status(str, Enum):
    PENDING = "pending"
    DONE = "done"

이렇게 하면 Status.PENDING == "pending"처럼 문자열 비교가 가능해져 웹 API에서 매우 유용하게 사용된다.

실무 활용 예: FastAPI

@app.get("/tasks/")
def get_tasks(status: Status):
    return {"status": status}
  • /tasks/?status=pending 요청에서 "pending"이 자동으로 Enum으로 매핑된다.
  • 직렬화 시 status: "pending"처럼 자연스럽게 문자열로 반환된다.

고급 기능들

1. auto() – 값 자동 부여

from enum import Enum, auto

class State(Enum):
    START = auto()
    PROCESSING = auto()
    END = auto()

값을 일일이 지정하지 않아도 순차적으로 자동 할당된다 (1, 2, 3, ...).


2. IntEnum, StrEnum

from enum import IntEnum

class Level(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
  • IntEnum은 숫자처럼 정렬, 덧셈 등이 가능
  • StrEnum은 Python 3.11부터 도입된 문자열 Enum

3. Flag / IntFlag – 비트 연산 가능 Enum

from enum import Flag, auto

class Permission(Flag):
    READ = auto()
    WRITE = auto()
    EXECUTE = auto()
perm = Permission.READ | Permission.WRITE
print(Permission.READ in perm)  # True

여러 값을 동시에 지정해야 할 때 유용 (예: 권한 조합)


4. 사용자 정의 메서드

class Mood(Enum):
    HAPPY = 1
    SAD = 2

    def emoji(self):
        return "😊" if self == Mood.HAPPY else "😢"

Enum 멤버에 직접 로직을 넣어 동작 정의 가능


5. 커스텀 속성 추가

class Country(Enum):
    KOREA = ("KR", "대한민국")
    JAPAN = ("JP", "일본")

    def __init__(self, code, label):
        self.code = code
        self.label = label
print(Country.KOREA.code)   # KR
print(Country.KOREA.label)  # 대한민국

코드값, 한글명, 설명 등 다양한 정보를 함께 저장 가능


6. 역방향 조회 (name, value)

print(Color["RED"])     # Color.RED
print(Color(1))         # Color.RED

Enum을 이름이나 값으로도 조회할 수 있음
JSON 역직렬화에 유용


📌 요약 테이블

기능 설명 실무 활용
auto() 값 자동 부여 반복 최소화
IntEnum, StrEnum 정수/문자열처럼 동작 숫자 비교, API 직렬화
Flag 비트 연산 지원 권한/설정 조합
커스텀 메서드 .emoji() 등 동작 정의 캡슐화된 로직
속성 추가 코드, 라벨 등 정의 다국어/선택지 표현
역방향 접근 이름/값으로 조회 직렬화 역변환

✨ 마무리

Python의 Enum은 단순히 고정된 값을 관리하는 것 이상의 기능을 제공한다.
특히 웹 개발, API 설계, 데이터 직렬화, 권한 제어, UI 표시 등에 있어 매우 강력하고 유용하다.
처음에는 생소할 수 있지만, 익숙해지면 더 안전하고 깔끔한 코드를 작성하실 수 있다.


🎁 보너스 팁

  • Enum은 불변(immutable)이다. 값을 변경하려 하면 에러가 난다.
  • Enum 멤버는 싱글턴(singleton)이므로 is 비교도 가능하다:
    Color.RED is Color.REDTrue
반응형

'Python > 문법' 카테고리의 다른 글

[Python] dataclass  (0) 2025.01.27
[Python] Module과 Package  (0) 2025.01.18
[Python] import 문 사용 팁  (0) 2025.01.13
[Python] Module import로 이해하는 Namespace와 Scope  (1) 2025.01.09
[Python] Unpacking의 다양한 예시  (2) 2024.12.27
반응형

의존성 관리는 프로젝트의 안정성과 재현성을 위해 매우 중요하다. 특히 팀 프로젝트나 배포 환경에서는 더욱 그렇다.

이 글에서는 의존성 관리의 핵심인 dependency 파일과 lock 파일에 대해 살펴본다.

 

Dependency 파일과 Lock 파일의 차이점

Dependency 파일

  • 개념: 프로젝트에 필요한 패키지와 대략적인 버전 범위를 정의
  • 파이썬 예시: requirements.txt, setup.py, pyproject.toml
  • 형식: 일반적으로 패키지 이름과 버전 제약 조건 포함 (예: Django>=3.2,<4.0)
  • 목적: 개발자가 프로젝트에 필요한 의존성을 명시적으로 선언

Lock 파일

  • 개념: 의존성 트리 전체의 정확한 버전을 고정(lock)
  • 파이썬 예시: poetry.lock, Pipfile.lock, pdm.lock
  • 형식: 모든 패키지와 그 하위 의존성의 정확한 버전, 해시값 등을 포함
  • 목적: 모든 환경에서 정확히 동일한 패키지 세트가 설치되도록 보장

 

Git에 포함해야 하나?

Dependency 파일

  • 결론: 반드시 포함해야 한다.
  • 이유: 프로젝트의 필수 요구사항을 정의하며, 다른 개발자가 프로젝트를 이해하고 설정하는 데 필수적이다.

Lock 파일

  • 결론: 대부분의 경우 포함해야 한다.
  • 이유:
    • 모든 개발자와 배포 환경에서 동일한 의존성을 보장
    • "내 컴퓨터에서는 잘 돌아가는데?" 문제 방지
    • 빌드 재현성 보장
  • 예외: 라이브러리를 개발할 때는 lock 파일을 제외하는 경우도 있다(사용자의 환경에 따라 유연하게 대응해야 하므로).

 

주요 의존성 관리 도구

파이썬

  1. pip + requirements.txt
    • 가장 기본적인 방식
    • Lock 파일 개념이 없음 (pip freeze로 생성한 파일은 유사하지만 완전한 lock 파일은 아님)
  2. Poetry
    • 현대적인 의존성 관리 도구
    • pyproject.toml과 poetry.lock 사용
    • 의존성 해결 알고리즘이 뛰어남
  3. Pipenv
    • Pipfile과 Pipfile.lock 사용
    • 가상환경 관리 통합
  4. PDM
    • PEP 621 표준 준수
    • pyproject.toml과 pdm.lock 사용
    • 의존성 관리와 빌드 시스템 통합
  5. uv
    • Rust로 작성된 최신 파이썬 패키지 설치 도구
    • pip보다 훨씬 빠른 설치 속도 제공
    • requirements.txt와 호환되며 lock 파일도 지원 (requirements.lock)
    • 가상환경 생성 및 관리 기능 통합
    • 의존성 해결 알고리즘이 개선되어 더 안정적인 의존성 트리 생성

 

다른 언어의 도구들

  • JavaScript: npm/yarn (package.json, package-lock.json/yarn.lock)
  • Ruby: Bundler (Gemfile, Gemfile.lock)
  • Go: Go Modules (go.mod, go.sum)
  • Java: Maven, Gradle (pom.xml, build.gradle)

 

하위 패키지 버전 결정 방식

패키지를 설치할 때 그 패키지가 필요로 하는 하위 패키지의 버전은 어떻게 결정될까?

의존성 해결 과정

  1. 의존성 트리 구성: 각 패키지가 필요로 하는 모든 하위 패키지를 재귀적으로 탐색
  2. 버전 제약 조건 수집: 모든 패키지의 버전 요구사항 수집
  3. 최적 버전 계산: 모든 제약 조건을 만족하는 각 패키지의 버전 조합 찾기

패키지 매니저별 동작 차이

  • pip: 설치 순서에 따라 결과가 달라질 수 있음 (최신 버전의 pip는 개선됨)
  • Poetry/Pipenv/PDM: 의존성 해결 알고리즘을 사용해 모든 제약조건을 만족하는 최적의 세트 계산
  • uv: 백트래킹 알고리즘을 사용하여 효율적이고 결정론적인 의존성 해결 제공, pip보다 훨씬 빠르게 의존성 계산 및 설치 수행, 동일한 입력에 대해 항상 동일한 결과 보장

예시

프로젝트에서 패키지 A와 B를 직접 사용
- 패키지 A는 C>=1.0,<2.0 필요
- 패키지 B는 C>=1.5,<3.0 필요

→ 이 경우 C의 1.5~1.9 버전이 설치됨 (보통은 범위 내 최신 버전)

 

의존성 충돌 해결하기

의존성 충돌은 패키지 간 호환되지 않는 버전 요구사항이 있을 때 발생한다.

일반적인 충돌 유형

패키지 A는 C==1.0 필요
패키지 B는 C==2.0 필요

→ 두 요구사항을 동시에 만족시킬 수 없음

해결 방법

  1. 패키지 업데이트
    • 충돌하는 패키지의 최신 버전으로 업데이트
    • 최신 버전에서는 종종 호환성 문제가 해결됨
  2. 버전 제약 조건 완화
    • 엄격한 버전 핀 대신 범위 지정 (예: ==1.0 → >=1.0,<2.0)
  3. 가상환경 분리
    • 호환되지 않는 의존성이 있는 경우 별도의 가상환경 사용
  4. 의존성 오버라이드
    • 일부 도구(Poetry, PDM)에서는 특정 패키지 버전을 명시적으로 오버라이드 가능
    # poetry.toml 예시
    [tool.poetry.dependencies]
    package-c = "1.5"  # A와 B 모두 호환되는 버전으로 강제
    
    # pyproject.toml (PDM) 예시
    [tool.pdm.overrides]
    package-c = "1.5"
    
  5. 직접 패치 또는 포크
    • 극단적인 경우, 문제가 있는 패키지를 직접 수정하거나 포크

 

실제 사용 예시: Django 프로젝트

Poetry 사용 시

1. 프로젝트 설정:

poetry init
poetry add django==4.2
poetry add --dev pytest pytest-django

2. 생성된 파일:

  • pyproject.toml: 주 의존성 파일
  • poetry.lock: 정확한 버전이 고정된 lock 파일

패키지 업데이트:

poetry update  # 모든 패키지를 제약조건 내에서 최신 버전으로 업데이트
poetry update django  # django만 업데이트

 

결론

Dependency 파일과 lock 파일을 적절히 활용하면 개발 과정이 더 원활해지고, "내 컴퓨터에서는 잘 돌아가는데?" 같은 문제를 피할 수 있다.

파이썬 백엔드 개발을 시작하는 분들께는 다음을 권장한다:

  1. 개인 프로젝트: 최소한 requirements.txt는 관리할 것.
  2. 팀 프로젝트: Poetry나 PDM 같은 현대적 도구로 lock 파일까지 관리할 것.
  3. 버전 관리: 의존성 파일과 lock 파일 모두 Git에 포함할 것.
반응형
반응형

Django로 웹 애플리케이션을 개발하다 보면 모델 필드를 정의할 때 null=Trueblank=True 옵션을 자주 마주치게 된다. 이 두 옵션은 비슷해 보이지만 실제로는 완전히 다른 목적을 가지고 있다.

두 속성의 차이점과 올바른 사용법에 대해 자세히 알아본다.

 

null=True와 blank=True의 핵심 차이

1. null=True

  • 작동 레벨: 데이터베이스 레벨
  • 의미: 해당 필드가 데이터베이스에서 NULL 값을 가질 수 있음
  • 영향: 데이터베이스 스키마에 직접적인 영향을 줌 (NULL 허용 컬럼으로 생성)
  • Python 표현: None 값이 해당 필드에 할당될 수 있음

2. blank=True

  • 작동 레벨: Django 폼 검증 레벨
  • 의미: 폼에서 해당 필드를 비워둘 수 있음 (필수 입력 항목이 아님)
  • 영향: 데이터베이스 스키마에 직접적인 영향을 주지 않음
  • 사용처: 관리자 페이지, ModelForm 등에서 필수 입력 항목 여부 결정

 

필드 타입별 권장 사용법

문자열 필드 (CharField, TextField)

문자열 필드는 Django에서 조금 특별하게 취급된다:

class Article(models.Model):
    title = models.CharField(max_length=100)  # 필수 필드
    subtitle = models.CharField(max_length=100, blank=True)  # 선택적 필드

이 경우:

  • subtitle은 폼에서 비워둘 수 있음 (blank=True)
  • 사용자가 값을 입력하지 않으면 빈 문자열('') 이 데이터베이스에 저장됨
  • null=True를 설정하지 않았으므로 데이터베이스에서는 NULL 값을 허용하지 않음

권장사항: 문자열 필드에는 보통 null=True를 사용하지 않는다. 빈 값을 나타내는 방법이 두 가지(NULL과 빈 문자열)가 되면 데이터 일관성에 혼란을 줄 수 있기 때문이다.

비문자열 필드 (IntegerField, DateField, BooleanField 등)

class Event(models.Model):
    name = models.CharField(max_length=100)
    attendees = models.IntegerField(null=True, blank=True)  # 선택적 숫자 필드
    event_date = models.DateField(null=True, blank=True)    # 선택적 날짜 필드

이 경우:

  • attendees와 event_date는 폼에서 비워둘 수 있음 (blank=True)
  • 값이 제공되지 않으면 데이터베이스에 NULL이 저장됨 (null=True)
  • Python 코드에서 이 필드들은 None 값을 가질 수 있음

권장사항: 숫자, 날짜, 불리언 등의 비문자열 필드에서 선택적 값을 허용하려면 null=Trueblank=True 둘 다 설정하는 것이 좋다.

 

실제 동작 예시

문자열 필드 (blank=True만 설정한 경우)

class Product(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)  # null=True 없음

폼에서 description을 비워두고 제출하면:

  • 데이터베이스에 빈 문자열('')이 저장됨
  • product.description의 값은 '' (빈 문자열)

날짜 필드 (null=True, blank=True 둘 다 설정한 경우)

class Order(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    order_date = models.DateField(auto_now_add=True)
    shipped_date = models.DateField(null=True, blank=True)

shipped_date를 지정하지 않고 주문을 생성하면:

  • 데이터베이스에 NULL이 저장됨
  • order.shipped_date의 값은 None

 

주의사항

  1. 일관성 유지하기: 문자열 필드에 null=Trueblank=True를 모두 설정하면 빈 값을 None과 빈 문자열('') 두 가지로 표현할 수 있게 되어 혼란을 줄 수 있다.
  2. 폼 검증: blank=True는 Django 폼 검증에만 영향을 주므로, 직접 모델 인스턴스를 생성하는 경우에는 영향이 없다.
  3. 기본값 고려하기: 선택적 필드지만 기본값이 있어야 하는 경우 default 매개변수를 사용할 것:
status = models.CharField(max_length=20, default='pending', blank=True)
priority = models.IntegerField(default=0, blank=True)

 

요약

Django 모델 필드에서 null=Trueblank=True의 차이점을 정리하면:

  1. null=True: 데이터베이스 레벨에서 NULL 값을 허용 (Python에서 None 값 허용)
  2. blank=True: 폼 검증 레벨에서 빈 값을 허용 (필수 입력 항목이 아님)

일반적인 권장사항:

  • 문자열 필드: blank=True만 사용 (빈 값은 빈 문자열로 저장)
  • 비문자열 필드: 선택적인 경우 null=True, blank=True 둘 다 사용

이러한 차이점을 이해하고 적절히 사용하면 Django 모델을 더 효과적으로 설계할 수 있다.

반응형
반응형

멀티 스테이지 빌드란?

멀티 스테이지 빌드는 Docker 17.05 버전에서 도입된 기능으로, 하나의 Dockerfile 내에서 여러 빌드 단계를 정의할 수 있게 해주는 기술이다. 이 기술을 사용하면 빌드 환경과 실행 환경을 분리하여 최종 이미지 크기를 대폭 줄이고 보안을 강화할 수 있다.

 

기존 Docker 빌드의 문제점

전통적인 Docker 빌드 방식은 단일 FROM 명령어로 시작하는 하나의 이미지를 사용한다. FastAPI 애플리케이션을 배포하는 상황을 가정했을 때, 단일 단계 Dockerfile의 예시를 살펴보자:

FROM python:3.13
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

이 방식에서는 여러 문제가 발생한다:

  1. 빌드 도구가 최종 이미지에 포함됨: pip install 명령을 실행할 때, 많은 파이썬 패키지(특히 numpy, pandas, psycopg2 등)은 C/C++ 확장을 컴파일해야 한다. 이 과정에서 gcc, g++, 개발 헤더 파일 등의 빌드 도구가 필요하며, 이들이 최종 이미지에 그대로 남게 된다.
  2. 중간 빌드 파일이 이미지에 포함됨: 패키지 설치 과정에서 생성된 소스 코드, 컴파일된 객체 파일(.o), 임시 빌드 디렉토리 등이 이미지 레이어에 그대로 남아 불필요하게 이미지 크기를 키운다.
  3. 보안 취약점 증가: 추가된 도구와 라이브러리는 잠재적인 공격 표면을 넓히고, 보안 취약점에 노출될 가능성을 높인다.
  4. 배포 및 확장 시 네트워크 부하 증가: 큰 이미지는 컨테이너 레지스트리에서 다운로드하고 노드 간에 전송하는 데 더 많은 시간이 소요된다.

 

멀티 스테이지 빌드의 작동 원리

멀티 스테이지 빌드의 핵심은 여러 FROM 명령어를 사용하여 서로 다른 기본 이미지에서 시작하는 여러 빌드 단계를 정의하는 것이다. 각 단계는 고유한 목적을 가지며, 이전 단계에서 생성된 특정 파일이나 결과물만 다음 단계로 복사된다.

멀티 스테이지 빌드를 사용하면:

  1. 빌드 단계에서만 빌드 도구를 사용: 컴파일러, 개발 헤더, 빌드 도구 등은 첫 번째 단계에서만 사용되고 최종 이미지에는 포함되지 않는다.
  2. 중간 빌드 파일 제외: 패키지 설치 과정에서 생성된 임시 파일과 빌드 아티팩트는 최종 이미지에 포함되지 않는다.
  3. 필요한 결과물만 복사: 빌드 단계에서 생성된 실행 파일이나 컴파일된 바이너리(wheel 등)만 최종 이미지로 복사된다.

아래 예시 Dockerfile을 통해 멀티 스테이지 빌드의 구조를 살펴보자:

# 빌드 단계
FROM python:3.13 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt

# 실행 단계
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /app/wheels /app/wheels
COPY . .
RUN pip install --no-cache-dir --no-index --find-links=/app/wheels -r requirements.txt && \
    rm -rf /app/wheels
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

 

멀티 스테이지 빌드 분석

위 Dockerfile의 내용을 분석해보자.

# 빌드 단계
FROM python:3.13 AS builder
WORKDIR /app
COPY requirements.txt .
# 의존성을 wheel 형태로 빌드
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt

# 실행 단계
FROM python:3.13-slim
WORKDIR /app
# 빌드 단계에서 생성된 wheel 파일만 복사
COPY --from=builder /app/wheels /app/wheels
COPY . .
# wheel 파일에서 패키지 설치 후 wheel 파일 삭제
RUN pip install --no-cache-dir --no-index --find-links=/app/wheels -r requirements.txt && \
    rm -rf /app/wheels
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

이 방식에서는:

  1. 컴파일러와 개발 도구는 빌드 단계에서만 사용되고 최종 이미지에는 포함되지 않는다.
  2. 미리 컴파일된 wheel 파일만 최종 이미지로 복사되므로 컴파일 과정의 중간 파일이 제외된다.
  3. 최종 이미지는 더 작은 기본 이미지(python:3.13-slim)를 사용하여 크기가 더 작다(종종 50-70% 감소).
  4. 불필요한 빌드 도구가 없어 보안이 강화된다.

단계별 분석

1. 빌드 단계 (Build Stage)

# 빌드 단계
FROM python:3.13 AS builder

여기서는 표준 Python 3.13 이미지를 기반으로 하는 첫 번째 단계를 시작하고, 이 단계에 builder라는 이름을 부여한다. 이 이름은 후속 단계에서 이 단계를 참조할 때 사용된다.

WORKDIR /app
COPY requirements.txt .

작업 디렉토리를 설정하고 필요한 의존성 파일을 복사한다.

RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt
이 명령은 프로젝트의 모든 의존성을 휠(wheel) 형태로 컴파일한다. 휠은 파이썬 패키지의 설치 가능한 바이너리 형식으로, 이후 단계에서 빠르게 설치할 수 있다.

휠(wheel)이 중요한 이유:

  • 많은 파이썬 패키지(numpy, pandas, psycopg2 등)는 C/C++ 확장을 포함하고 있어 컴파일이 필요하다.
  • 휠은 이미 컴파일된 바이너리 형식이므로, 설치 시 컴파일 과정이 필요하지 않다.
  • 휠을 사용하면 빌드 도구(gcc, g++ 등)와 개발 헤더 파일 없이도 패키지를 설치할 수 있다.

2. 실행 단계 (Runtime Stage)

FROM python:3.13-slim

두 번째 단계는 python:3.13-slim이라는 더 작은 기본 이미지로 시작한다. 이 이미지는 표준 Python 이미지보다 훨씬 작으며, 필수적인 Python 런타임만 포함하고 있다.

WORKDIR /app
COPY --from=builder /app/wheels /app/wheels

이 명령은 첫 번째 단계(builder)에서 생성한 휠 파일들을 현재 단계로 복사한다. --from=builder는 이 파일들의 출처가 builder 단계라는 것을 Docker에 알려준다.

COPY . .

현재 호스트 시스템의 모든 애플리케이션 코드를 복사한다.

RUN pip install --no-cache-dir --no-index --find-links=/app/wheels -r requirements.txt && \
    rm -rf /app/wheels

복사된 휠 파일을 사용하여 의존성을 설치한 다음, 휠 디렉토리를 삭제하여 이미지 크기를 더 줄인다.

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

마지막으로 컨테이너가 사용할 포트를 노출하고, FastAPI 애플리케이션을 Uvicorn 서버로 실행하는 명령을 지정한다.

 

멀티 스테이지 빌드의 장점

1. 이미지 크기 최소화

멀티 스테이지 빌드의 가장 큰 장점은 최종 이미지 크기의 대폭 감소이다. 빌드 도구, 컴파일러, 개발 헤더 파일 등 런타임에 필요하지 않은 모든 것이 최종 이미지에서 제외된다.

2. 보안 강화

더 작은 이미지는 보안 측면에서도 이점을 제공한다. 불필요한 도구와 라이브러리가 없으므로 공격 표면이 줄어들고, 취약점 발생 가능성도 낮아진다. 또한 빌드 과정에서 발생할 수 있는 보안 이슈가 최종 이미지에 영향을 미치지 않는다.

3. 구성 단순화

빌드 단계와 실행 단계를 별도의 Dockerfile로 나누는 대신, 하나의 파일로 모든 것을 관리할 수 있다. 이는 CI/CD 파이프라인과 배포 프로세스를 단순화한다.

4. 빌드 캐시 최적화

각 빌드 단계는 Docker의 레이어 캐싱 시스템을 활용한다. 소스 코드만 변경된 경우, 의존성 빌드 단계는 캐시에서 재사용되어 빌드 시간이 단축된다.

 

결론

멀티 스테이지 빌드는 Docker 이미지 최적화를 위한 강력한 도구이다. 특히 Django, FastAPI와 같은 파이썬 백엔드 프레임워크를 사용하는 개발자에게 큰 이점을 제공한다. 더 작고, 더 안전하며, 더 효율적인 컨테이너를 만들어 배포 프로세스를 개선하고 리소스 사용을 최적화할 수 있다.

빌드 도구와 중간 파일을 효과적으로 제거하는 멀티 스테이지 빌드의 접근 방식은 특히 C 확장을 포함하는 파이썬 패키지를 사용하는 애플리케이션에서 큰 차이를 만든다. 이를 통해 개발 환경과 실행 환경을 명확히 분리하고, 필요한 결과물만 최종 이미지에 포함시킬 수 있게 된다.

반응형
반응형

Django ORM에서 select_related와 prefetch_related는 모두 쿼리 최적화를 위한 도구지만, 서로 다른 방식으로 작동한다. 둘의 내부원리 및 동작방식을 비교하고, 언제 각각을 사용해야 하는지 알아본다.

 

공통점

 

  • 쿼리 최적화 도구: 두 메서드 모두 Django ORM의 쿼리 최적화 도구로, 데이터베이스 쿼리 횟수의 감소가 목적이다.
  • N+1 문제 해결: 두 메서드 모두 반복문에서 각 객체마다 추가 쿼리가 발생하는 N+1 쿼리 문제를 해결한다.
  • 즉시 로딩(Eager loading): 두 메서드 모두 지연 로딩(lazy loading) 대신 즉시 로딩 방식을 사용하여 필요한 데이터를 미리 가져온다.
  • 체이닝 가능: 두 메서드 모두 다른 쿼리셋 메서드들과 체이닝이 가능하다.
Post.objects.select_related('author').filter(is_published=True)
User.objects.prefetch_related('posts').order_by('username')
  • 성능 향상: 두 메서드 모두 관련 객체 접근시 추가 데이터베이스 쿼리를 방지하여 애플리케이션 성능을 향상시킨다.
  • 메모리 캐싱: 두 메서드 모두 관련 객체 데이터를 메모리에 캐싱하여 재사용 가능하게 한다.
  • 함께 사용 가능: 두 메서드는 함께 사용할 수 있으며, 복잡한 모델 관계에서는 종종 두 메서드를 모두 사용한다.

 

Post.objects.select_related('author').prefetch_related('comments', 'tags')

 

내부 원리와 동작방식의 차이

select_related

  • SQL JOIN 사용: 단일 SQL 쿼리 내에서 외래 키 관계를 JOIN으로 처리한다.
  • 정방향 참조(Forward relation)에 최적화: 주로 ForeignKey, OneToOneField 관계에 사용된다.
  • 즉시 로딩(Eager loading): 메인 쿼리 실행 시 관련 객체도 함께 가져온다.

prefetch_related

  • 별도의 쿼리 실행: 먼저 주 쿼리를 실행한 후, 관련 객체들을 별도의 쿼리로 가져온다.
  • 역방향 참조에 적합: ManyToManyField나 역방향 ForeignKey 관계(related_name으로 접근하는 관계)에 적합하다.
  • 파이썬 메모리에서 JOIN: 데이터베이스가 아닌 파이썬에서 두 쿼리 결과를 결합한다.

 

모델 코드

먼저 아래와 같은 모델 코드를 먼저 가정한다.

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)
    

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    tags = models.ManyToManyField(Tag, blank=True, related_name='posts')


class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

 주요 모델 관계:

  1. OneToOne 관계:
    • User와 Profile 사이 (한 유저는 하나의 프로필만 가짐)
  2. ForeignKey(일대다) 관계:
    • Post와 User 사이 (한 유저는 여러 포스트 작성 가능)
    • Post와 Category 사이 (한 카테고리는 여러 포스트 포함 가능)
    • Comment와 Post 사이 (한 포스트는 여러 댓글 가질 수 있음)
    • Comment와 User 사이 (한 유저는 여러 댓글 작성 가능)
  3. ManyToMany(다대다) 관계:
    • Post와 Tag 사이 (한 포스트는 여러 태그 가질 수 있고, 한 태그는 여러 포스트에 사용 가능)

 

select_related는 언제 사용?

  • 단일 객체에 대한 외래 키 관계를 조회할 때 (ForeignKey, OneToOne)
  • 관련 객체가 항상 필요하고 관계의 깊이가 깊지 않을 때

예시

# 단일 외래 키 관계
# 각 게시물의 작성자 정보를 한 번에 가져옴
posts = Post.objects.select_related('author').all()

# 중첩된 관계도 가능
posts = Post.objects.select_related('author__profile').all()

# 여러 관계 동시에 가져오기
posts = Post.objects.select_related('author', 'category').all()

select_related는 JOIN을 사용하므로 더 큰 결과 셋을 반환할 수 있지만, 쿼리는 1번만 실행된다.

  • SQL JOIN의 동작 방식: JOIN은 두 테이블의 관련 행을 결합하므로, 관계에 따라 결과 행 수가 증가할 수 있다. 특히 일대다 관계에서는 "다" 측 항목만큼 행이 증가한다.
  • 중복 데이터 발생: 예를 들어 Post.objects.select_related('author')에서:
    • 한 저자가 여러 게시물을 작성했다면, 저자 정보가 각 게시물마다 중복되어 반환됨
    • 결과적으로 데이터 크기가 커지지만, 이는 단일 SQL 쿼리로 모든 정보를 가져오기 위해 발생하는 tradeoff임
  • N+1 문제 해결: 별도의 쿼리 없이 관련 객체에 접근할 수 있어, 반복문에서 각 객체마다 새 쿼리를 실행하는 N+1 문제를 해결한다.
  • 즉시 로딩(Eager loading): 필요한 모든 데이터를 한 번에 가져와 캐시하므로 데이터베이스 왕복 시간을 줄이는 장점이 있다.

결국 select_related는 데이터베이스 호출 횟수를 최소화하는 대신, 더 많은 데이터를 한번에 가져오는 tradeoff가 있다.

 

prefetch_related는 언제 사용?

  • 역방향 관계나 다대다 관계를 조회할 때
  • 특히 Post.objects.all()의 각 항목에 대해 여러 관련 객체가 필요할 때

예시

# 역방향 관계 (한 사용자의 모든 게시물)
users = User.objects.prefetch_related('posts').all()

# ManyToMany 관계
posts = Post.objects.prefetch_related('tags').all()

# 복잡한 중첩 prefetch
users = User.objects.prefetch_related(
    'posts',
    'posts__comments',
    'posts__tags'
).all()

prefetch_related는 각 관계마다 별도 쿼리를 실행하지만, 최종 JOIN은 메모리에서 이루어져 복잡한 관계에 더 효율적일 수 있다.

  • 데이터베이스 부하 감소: 복잡한 다대다 관계나 역방향 관계를 SQL JOIN으로 처리하면 카테시안 곱(Cartesian product)이 발생하여 결과 행 수가 기하급수적으로 증가할 수 있다. 이는 데이터베이스 서버에 큰 부담을 준다.
  • 중복 데이터 최소화: 별도 쿼리를 실행하면 각 객체를 한 번만 가져와 파이썬에서 관계를 구성하므로, 네트워크를 통해 전송되는 중복 데이터가 감소한다.
  • 메모리 효율성: 예를 들어, 한 사용자가 1,000개의 게시물을 가진 경우:
    • select_related로 JOIN 시: 1,000개 행이 반환되고 사용자 데이터가 1,000번 중복됨
    • prefetch_related 사용 시: 사용자 1개 + 게시물 1,000개를 별도로 가져와 파이썬에서 연결
  • 쿼리 최적화: 각 테이블에 최적화된 쿼리를 별도로 실행할 수 있어 인덱스 활용이 더 효율적이다.

복잡한 관계에서 select_related 사용시 SQL JOIN은 성능 저하의 원인이 될 수 있으므로, Django는 prefetch_related로 이를 파이썬 메모리에서 처리하는 방식으로 해결한다.

 

두 메서드를 적절히 활용하면 N+1 쿼리 문제를 해결하고 Django 애플리케이션의 성능을 크게 향상시킬 수 있다.

반응형
반응형

Django Debug Toolbar 는 Django 개발 시 디버깅을 도와주는 매우 유용한 도구이다.

https://django-debug-toolbar.readthedocs.io

settings.py에서 Django Debug Toolbar를 Debug=True일 때 활성화하면, 브라우저 환경에서 Django 개발에 필요한 다양한 디버깅 옵션을 사용할 수 있다.

 

그런데 종종 윈도우 개발 환경에서 아래와 같은 문제가 발생할 수 있다.

 

문제 상황

개발 서버 실행시, 아래와 같은 오류 메시지가 뜨면서 Django Debug Toolbar가 로드에 실패할 수 있다.

$ python manage.py runserver

Watching for file changes with StatReloader
Performing system checks...

System check identified some issues:

WARNINGS:
?: (debug_toolbar.W007) JavaScript files are resolving to the wrong content type.
        HINT: The Django Debug Toolbar may not load properly while mimetypes are misconfigured. See the Django documentation for an explanation of why this occurs.
https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#static-file-development-view

This typically occurs on Windows machines. The suggested solution is to modify HKEY_CLASSES_ROOT in the registry to specify the content type for JavaScript files.

[HKEY_CLASSES_ROOT\.js]
"Content Type"="application/javascript"

오류 메시지에도 나오듯이, 이는 Windows 환경에서 종종 나타나는 문제이다. 

이는 JavaScript 파일의 MIME 타입이 올바르게 설정되지 않아서 발생하는 현상이다.

 

MIME 타입이란?

MIME 타입(Multipurpose Internet Mail Extensions Type)은 파일의 형식을 식별하는 표준화된 방법이다. 웹에서 특히 중요한 역할을 하는데, 브라우저가 받은 파일을 어떻게 처리해야 할지 결정하는 데 사용된다.

쉽게 설명하면:

  1. 브라우저가 서버에서 파일을 받을 때, "이게 어떤 종류의 파일이야?"라고 물어보는 것과 같다.
  2. 서버는 MIME 타입을 통해 "이건 JavaScript 파일이야" 또는 "이건 이미지 파일이야"라고 알려준다.
  3. 브라우저는 이 정보를 바탕으로 파일을 적절히 처리한다.

 

즉 위 에러는, 서버가 js 파일에 대해 application/json이 아닌 다른 text/plain과 같은 타입으로 브라우저에 응답을 해서, 브라우저가 js 파일 로딩을 거부한 상황이다.

 

해결 방법

1. 레지스트리 편집기로 JS의 MIME 타입 수정하기(가장 근본적인 해결 방법)

1. Windows + R 키를 눌러 실행 창을 열기
2. 'regedit' 입력하고 실행
3. HKEY_CLASSES_ROOT\.js 위치로 이동
4. 마우스 우클릭 > 새로 만들기 > 문자열 값
5. 이름을 'Content Type' 으로 설정
6. 값을 'application/javascript' 로 설정

아래와 같이 .js의 Content Type이 application/javascript로 수정되어야 한다. 

 

2. settings.py에 다음 설정을 추가(임시 해결 방법)

STATICFILES_HANDLERS = (
    'django.contrib.staticfiles.handlers.StaticFilesHandler',
)
  • Django의 개발 서버가 정적 파일(CSS, JavaScript 등)을 어떻게 처리할지 지정하는 설정이다.
  • StaticFilesHandler는 Django의 기본 정적 파일 처리기로, 파일의 MIME 타입을 좀 더 정확하게 처리한다.
  • Windows의 MIME 타입 설정을 우회하고 Django가 직접 처리하도록 하는 방식이다.

 

 

3. mimetypes 모듈을 사용한 임시 코드 추가 (임시 해결 방법)

settings.py에 아래 내용을 추가한다.

import mimetypes

mimetypes.add_type("application/javascript", ".js", True)
  • 파이썬의 mimetypes 모듈을 사용해서 파일 확장자와 MIME 타입의 매핑을 직접 추가하는 방식이다.
  • ".js" 확장자를 가진 파일은 "application/javascript" MIME 타입으로 처리하라고 파이썬에 알려주는 것이다.
  • 마지막 매개변수 True는 이 매핑을 강제로 적용하라는 의미이다.

 

2번, 3번 방법 모두 Windows의 레지스트리를 수정하지 않고도 JavaScript 파일이 올바른 MIME 타입으로 처리되도록 하는 방법이다. 둘 중에서는 더 직접적이고 명확한 3번 방법이 권장된다.

 

반응형
반응형

Django 개발시 새로운 데이터를 생성하는 기능을 자주 구현하게 된다. 예를 들어 블로그 포스트 작성, 사용자 등록, 댓글 작성 등이 있다. Django는 이런 생성 로직을 쉽게 구현할 수 있도록 CreateView라는 제너릭 뷰를 제공한다.

 

CreateView란?

CreateView는 Django의 클래스 기반 뷰(CBV) 중 하나로, 새로운 객체를 생성하기 위한 폼을 표시하고 제출된 데이터를 처리하는 뷰이다. 함수 기반 뷰(FBV)로 직접 구현하면 꽤 많은 코드가 필요한 기능들을 CreateView는 손쉽게 제공한다.

 

1. 기본 설정하기

CreateView를 사용하기 위한 가장 기본적인 설정은 다음과 같다:

from django.views.generic.edit import CreateView
from .models import MyModel

class MyCreateView(CreateView):
    model = MyModel
    fields = ['field1', 'field2']  # 입력받을 필드들
    template_name = 'myapp/create.html'  # 사용할 템플릿
    success_url = '/success/'  # 생성 성공 후 리다이렉트할 URL

Django의 generic view인 CreateView 클래스를 import해오고, 이를 상속받아 새로운 커스텀 뷰를 만든다. 이렇게 만든 클래스는 CRUD 중 Create 기능을 담당한다.

각 필드의 설명은 아래와 같다:

  • model: 어떤 모델의 객체를 생성할지 지정한다. 여기서는 MyModel 모델의 인스턴스가 생성되게 된다.
  • fields: 폼에서 입력받을 필드들을 지정한다. 사용자에게 이 필드들만 입력받아 객체를 생성한다. 모델의 모든 필드가 아닌 필요한 필드만 지정할 수 있다.
  • template_name: 입력 폼을 보여줄 템플릿 파일을 지정한다. 이 템플릿에서 {{ form }}을 사용하면 자동으로 폼이 생성된다.
  • success_url: 객체 생성이 성공했을 때 리다이렉트할 URL을 지정한다. 

이처럼 필드 지정만으로, 폼 처리, 유효성 검사, 객체 저장 등의 일반적인 작업이 자동으로 처리된다. 즉, 최소한의 코드로 생성 기능을 구현할 수 있다.

 

2. HTTP 메서드별 동작 방식

GET 요청 시:

  • CreateView는 지정된 model을 기반으로 빈 ModelForm을 생성한다.
  • 이 폼을 템플릿에 전달하여 사용자에게 입력 폼을 보여준다.
  • 템플릿에서는 {{ form }}으로 접근할 수 있다.

POST 요청 시:

  • 사용자가 제출한 데이터의 유효성을 검사한다.
  • 폼에 입력된 내용이 유효한(valid) 경우: 새로운 객체를 생성하고 success_url로 리다이렉트한다
  • 폼에 입력된 내용이 유효하지 않은(invalid) 경우: 에러 메시지와 함께 폼을 다시 보여준다.

 

3. 실제 사용 예시 

# urls.py
path('create/', MyCreateView.as_view(), name='create')

# create.html
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">저장</button>
</form>

 

4. 주요 커스터마이징 포인트 

CreateView는 다양한 메서드를 오버라이드하여 동작을 커스터마이징할 수 있다. 몇가지 예시를 만들어봤다:

  • form_valid(self, form): 폼 데이터가 유효할 때 실행되는 메서드
  • get_success_url(self): 성공 시 리다이렉트할 URL을 동적으로 지정
  • get_form_class(self): 사용할 폼 클래스를 커스텀하게 지정
  • get_initial(self): 폼의 초기값을 설정
  • get_form_kwargs(self): 폼 인스턴스를 생성할 때 전달될 키워드 인자들을 결정

 

from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import MyModel
from .forms import MyCustomForm  # 커스텀 폼 클래스
from django.contrib import messages  # 메시지 프레임워크

class MyCreateView(CreateView):
    model = MyModel
    fields = ['field1', 'field2']
    template_name = 'myapp/create.html'
    
    def form_valid(self, form):
        """폼이 유효할 때 실행되는 메서드"""
        # 저장하기 전에 현재 로그인한 사용자를 작성자로 지정
        form.instance.author = self.request.user
        
        # 추가적인 데이터 처리
        form.instance.status = 'draft'
        
        # 부모 클래스의 form_valid 호출 (실제 저장 발생)
        response = super().form_valid(form)
        
        # 성공 메시지 추가
        messages.success(self.request, '성공적으로 생성되었습니다!')
        
        return response

    def get_success_url(self):
        """성공 시 이동할 URL을 동적으로 반환"""
        # 새로 생성된 객체의 detail 페이지로 이동
        return reverse_lazy('myapp:detail', kwargs={'pk': self.object.pk})
        
        # 또는 사용자 타입에 따라 다른 페이지로 이동
        if self.request.user.is_staff:
            return reverse_lazy('myapp:admin_list')
        return reverse_lazy('myapp:user_list')

    def get_form_class(self):
        """사용할 폼 클래스를 결정"""
        # 조건에 따라 다른 폼 클래스 반환
        if self.request.user.is_staff:
            return MyCustomForm  # 관리자용 커스텀 폼
        return super().get_form_class()  # 기본 모델폼

    def get_initial(self):
        """폼의 초기값을 설정"""
        initial = super().get_initial()  # 기본 초기값
        
        # 현재 시간을 created_at 필드의 초기값으로 설정
        initial['created_at'] = timezone.now()
        
        # 현재 로그인한 사용자 정보로 초기값 설정
        initial['author_name'] = self.request.user.get_full_name()
        
        # URL 파라미터에서 카테고리 값을 가져와서 설정
        initial['category'] = self.request.GET.get('category', '')
        
        return initial
        
    def get_form_kwargs(self):
        """폼 생성 시 전달할 키워드 인자들을 반환"""
        # 기본 kwargs 가져오기 (부모 클래스의 처리 결과)
        kwargs = super().get_form_kwargs()

        # 현재 로그인한 사용자 전달
        kwargs['user'] = self.request.user

        # 추가 컨텍스트 전달
        kwargs['category'] = self.request.GET.get('category', 'general')
        
        # request 객체 전달
        kwargs['request'] = self.request

        return kwargs

각 메서드의 설명:

  1. form_valid(self, form):
    • 폼 데이터가 유효할 때 실행된다.
    • 객체를 저장하기 전에 추가적인 데이터 처리가 가능하다.
    • 현재 로그인한 사용자를 작성자로 지정하는 등의 작업을 수행할 수 있다.
    • messages 프레임워크를 사용해 사용자에게 알림을 보낼 수 있다.
  2. get_success_url(self):
    • 객체 생성 성공 후 리다이렉트할 URL을 동적으로 결정한다.
    • 새로 생성된 객체의 정보를 사용할 수 있다(self.object로 접근).
    • 사용자 권한 등에 따라 다른 페이지로 보낼 수 있다.
  3. get_form_class(self):
    • 사용할 폼 클래스를 동적으로 결정한다.
    • 사용자 권한이나 조건에 따라 다른 폼을 사용할 수 있다.
    • 기본 모델폼 대신 커스텀 폼을 사용할 수 있다.
  4. get_initial(self):
    • 폼의 초기값을 설정한다.
    • 현재 시간, 사용자 정보 등을 기본값으로 설정할 수 있다.
    • URL 파라미터에서 값을 가져와서 초기값으로 설정할 수 있다.
    • 부모 클래스의 initial 값을 상속받아 확장할 수 있다.
  5. get_form_kwargs(self):
    • 폼 인스턴스를 생성할 때 전달될 키워드 인자들을 결정한다.
    • 사용자 정보를 전달할 수 있다.
    • URL 파라미터에서 값을 전달할 수 있다.
    • 추가 컨텍스트를 전달할 수 있다.
    • 외부 데이터를 전달 할 수 있다.
    • 전달한 kwargs를 사용하기 위해서는 아래와 같은 커스텀 폼 클래스가 필요하다:
from django import forms
from .models import MyModel

class MyCustomForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ['field1', 'field2']

    def __init__(self, *args, **kwargs):
        # kwargs에서 추가 데이터 추출
        self.user = kwargs.pop('user', None)
        self.category = kwargs.pop('category', None)
        self.request = kwargs.pop('request', None)

        # 부모 클래스의 __init__ 호출
        super().__init__(*args, **kwargs)

        # 추가 데이터를 기반으로 폼 필드 커스터마이징
        if self.user and not self.user.is_staff:
            self.fields['field1'].widget.attrs['readonly'] = True

        if self.category == 'notice':
            self.fields['field1'].label = '공지사항 제목'

 

정리

CreateView는 객체 생성과 관련된 코드 작성을 크게 줄여준다. fields를 사용한 간단한 구현부터 form_class를 사용한 복잡한 커스터마이징까지, 상황에 맞는 적절한 방법을 선택할 수 있다.

CreateView를 효과적으로 사용하기 위해서는 아래 규칙들을 잘 지키는 것이 좋다:

  1. 모델과 필드를 명확히 정의하기
  2. 필요한 경우 커스텀 폼 클래스 생성하기
  3. 적절한 유효성 검사 규칙 추가하기
  4. 사용자 경험을 고려한 템플릿 디자인하기
  5. 성공/실패 시의 동작 정의하기
반응형
반응형

Django 프로젝트에서 PostgreSQL을 연동하는 방법을 알아본다. 윈도우 환경에서 진행한다.

 

1. PostgreSQL 설치하기

먼저 PostgreSQL을 설치해야 한다:

  1. https://www.postgresql.org/download/windows/ 에서 installer 다운로드
  2. 설치 과정에서 입력한 비밀번호는 반드시 메모해둬야 한다.
  3. 기본 포트 번호는 5432를 사용한다.

 

2. 시스템 환경변수 확인

PostgreSQL 설치 후 PATH 설정이 필요하다. psql 명령어가 인식되지 않는다면, "시스템 환경변수 편집"에서 PATH에 psql의 위치(C:\Program Files\PostgreSQL\17\bin)를 추가한다. (17은 설치한 PostgreSQL 버전에 따라 다를 수 있다)

이제 새로운 명령 프롬프트를 열고 다음 명령어로 PostgreSQL이 제대로 설치되었는지 확인한다.

psql --version

 

3. Django 프로젝트 설정

3.1 필요한 패키지 설치

pip install psycopg2-binary

3.2 데이터베이스 생성

PostgreSQL 명령 프롬프트에서 다음 명령어를 실행해서 DB를 생성한다:

CREATE DATABASE myproject;

pgAdmin에서 DB를 생성할 수도 있다.

3.3 Django 설정 변경

Django 프로젝트의 settings.py 파일에서 데이터베이스 설정을 수정한다:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myproject',
        'USER': 'postgres',
        'PASSWORD': '설치_시_입력한_비밀번호',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

 

4. 보안 설정

프로젝트 보안을 위해 환경 변수를 사용하는 것을 추천한다:

pip install python-dotenv

프로젝트 루트에 .env 파일을 생성한다:

DB_NAME=myproject
DB_USER=postgres
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=5432

settings.py 파일을 다음과 같이 수정한다:

import os
from dotenv import load_dotenv

load_dotenv()

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
        'PORT': os.getenv('DB_PORT'),
    }
}

 

5. 마이그레이션 실행

설정이 완료되었다면, 다음 명령어로 마이그레이션을 실행한다:

python manage.py makemigrations
python manage.py migrate

 

6. 연결 테스트

Django 쉘에서 데이터베이스 연결을 테스트해볼 수 있다:

from django.db import connection

try:
    connection.ensure_connection()
    print("데이터베이스 연결 성공!")
    
    # 현재 데이터베이스 정보 확인
    print(f"현재 사용 중인 데이터베이스: {connection.settings_dict['NAME']}")
    print(f"데이터베이스 엔진: {connection.vendor}")
    
    # 간단한 쿼리 실행
    with connection.cursor() as cursor:
        cursor.execute("SELECT version();")
        version = cursor.fetchone()
        print(f"PostgreSQL 버전: {version[0]}")
        
except Exception as e:
    print(f"연결 실패: {str(e)}")
데이터베이스 연결 성공!
현재 사용 중인 데이터베이스: myproject
데이터베이스 엔진: postgresql
PostgreSQL 버전: PostgreSQL 17.2 on x86_64-windows, compiled by msvc-19.42.34435, 64-bit

 

주의사항

  1. .env 파일은 반드시 .gitignore에 추가하기
  2. PostgreSQL 설치 시 설정한 비밀번호는 안전한 곳에 보관하기
  3. 새로운 명령 프롬프트를 열었을 때 psql 명령어가 인식되지 않는다면, PATH 설정을 다시 확인할 것
  4. 데이터베이스 접속이 안 될 경우 PostgreSQL 서비스가 실행 중인지 확인하기 (Windows 서비스에서 "PostgreSQL" 확인)

이렇게 하면 Django 프로젝트에서 PostgreSQL을 사용할 준비가 완료된다!

더 나아가 pgAdmin을 설치하면 GUI 환경에서 편리하게 데이터베이스를 관리할 수 있다. pgAdmin은 PostgreSQL 설치 시 함께 설치되었을 것이다.

반응형
반응형

https://www.acmicpc.net/problem/14267

트리형 데이터 구조에서, 루트 노드부터 말단 노드들까지 값을 전파해서 각 노드들이 자신에게 전파된 모든 값의 합을 구하는 문제이다.

데이터 인풋이 좀 이상한 문제라, 디버깅하는데 시간을 많이 썼다. 인풋이 M줄 들어오고, M줄의 인풋만 읽어야 하는데, 실제로 인풋이 몇 줄 더 들어와서 sys.stdin을 for문으로 돌면 더 많은 인풋을 읽게되는 케이스가 있었다.

 

아무튼 트리 자료구조를 구현한 뒤, DFS를 사용하면 풀 수 있는 문제이다.

 

정답 코드 1 - DFS를 재귀 함수로 구현

import sys

sys.setrecursionlimit(10**6)

n, m = map(int, next(sys.stdin).split())
graph = {i: [] for i in range(1, n + 1)}
for employee, boss in enumerate(map(int, next(sys.stdin).split()[1:]), start=2):
    graph[boss].append(employee)

compliments = [0] * (n + 1)

for _ in range(m):
    employee, compliment = map(int, next(sys.stdin).split())
    compliments[employee] += compliment


def dfs(boss):
    for employee in graph[boss]:
        compliments[employee] += compliments[boss]
        dfs(employee)


dfs(1)
print(*compliments[1:])

 

정답 코드 2 - DFS를 스택을 사용해 구현

import sys
from collections import deque

n, m = map(int, next(sys.stdin).split())
graph = {i: [] for i in range(1, n + 1)}
for employee, boss in enumerate(map(int, next(sys.stdin).split()[1:]), start=2):
    graph[boss].append(employee)

compliments = [0] * (n + 1)

for _ in range(m):
    employee, compliment = map(int, next(sys.stdin).split())
    compliments[employee] += compliment

stack = deque([1])
while stack:
    boss = stack.pop()  # popleft() 사용시 BFS
    for employee in graph[boss]:
        compliments[employee] += compliments[boss]
        stack.append(employee)

print(*compliments[1:])

성능차이는 별로 없는 것 같다. 재귀함수를 사용하지 않으니, 콜스택 반복 생성에 의한 메모리 사용이 좀 줄어든 것 같다.

참고로 위 코드의 주석에도 써놨다시피, 이 문제는 큐를 이용한 BFS로 풀 수 있기도 하다.

반응형
반응형

https://www.acmicpc.net/problem/28279

큐를 구현하는 문제인데, 아래의 기능들이 수행 가능해야 한다.

  • 앞과 뒤로 요소를 조회 
  • 앞과 뒤로 요소를 추가(insert)
  • 앞과 뒤로 요소를 삭제(pop)
  • 큐가 비었는지 확인
  • 큐의 요소 갯수 확인

 

오랜만에 재미삼아 옛날에 주 언어였던 C언어로 직접 이중연결리스트를 구현해서 풀어봤다.

정답 코드

#include <stdio.h>
#include <stdlib.h>

typedef struct node {
  int data;
  struct node *prev;
  struct node *next;
} Node;

typedef struct llist {
  Node *head;
  Node *tail;
  int count;
} Llist;

Llist *llist_init() {
  Node *head = malloc(sizeof(Node));
  Node *tail = malloc(sizeof(Node));
  head->prev = NULL;
  head->next = tail;
  tail->prev = head;
  tail->next = NULL;
  Llist *llist = malloc(sizeof(Llist));
  llist->head = head;
  llist->tail = tail;
  llist->count = 0;
  return llist;
}

int llist_is_empty(Llist *llist) {
  return (llist->head->next == llist->tail) ? 1 : 0;
}

void llist_insert_front(Llist *llist, int data) {
  Node *node = malloc(sizeof(Node));
  node->data = data;
  node->next = llist->head->next;
  llist->head->next->prev = node;
  node->prev = llist->head;
  llist->head->next = node;
  llist->count++;
}

void llist_insert_rear(Llist *llist, int data) {
  Node *node = malloc(sizeof(Node));
  node->data = data;
  node->prev = llist->tail->prev;
  llist->tail->prev->next = node;
  node->next = llist->tail;
  llist->tail->prev = node;
  llist->count++;
}

int llist_pop_front(Llist *llist) {
  if (llist_is_empty(llist)) return -1;
  Node *remove_node = llist->head->next;
  remove_node->next->prev = llist->head;
  llist->head->next = remove_node->next;
  int data = remove_node->data;
  free(remove_node);
  llist->count--;
  return data;
}

int llist_pop_rear(Llist *llist) {
  if (llist_is_empty(llist)) return -1;
  Node *remove_node = llist->tail->prev;
  remove_node->prev->next = llist->tail;
  llist->tail->prev = remove_node->prev;
  int data = remove_node->data;
  free(remove_node);
  llist->count--;
  return data;
}

int llist_count(Llist *llist) { return llist->count; }

int llist_peek_front(Llist *llist) {
  return llist_is_empty(llist) ? -1 : llist->head->next->data;
}

int llist_peek_rear(Llist *llist) {
  return llist_is_empty(llist) ? -1 : llist->tail->prev->data;
}

void llist_traverse_print(Llist *llist) {
  for (Node *node = llist->head->next; node != llist->tail; node = node->next)
    printf("%d\n", node->data);
}

int run_command(Llist *llist, Llist *answer_list, int cmd) {
  int x;
  switch (cmd) {
    case 1:
      scanf("%d", &x);
      llist_insert_front(llist, x);
      break;
    case 2:
      scanf("%d", &x);
      llist_insert_rear(llist, x);
      break;
    case 3:
      llist_insert_rear(answer_list, llist_pop_front(llist));
      break;
    case 4:
      llist_insert_rear(answer_list, llist_pop_rear(llist));
      break;
    case 5:
      llist_insert_rear(answer_list, llist_count(llist));
      break;
    case 6:
      llist_insert_rear(answer_list, llist_is_empty(llist));
      break;
    case 7:
      llist_insert_rear(answer_list, llist_peek_front(llist));
      break;
    case 8:
      llist_insert_rear(answer_list, llist_peek_rear(llist));
      break;
  }
}

int main() {
  Llist *llist = llist_init();
  Llist *answer_list = llist_init();
  int n;
  scanf("%d", &n);
  while (n--) {
    int cmd;
    scanf("%d", &cmd);
    run_command(llist, answer_list, cmd);
  }
  llist_traverse_print(answer_list);
  return 0;
}

애써 만든 이중연결리스트가 아까워서, 정답을 출력하는 부분에서도, 이를 활용했다.

문제를 풀면서, 역시 C언어로 뭔가를 구현하는 건 너무 힘들다고 느꼈다. 파이썬이면 collections의 deque를 사용해서 간단히 풀 수 있을텐데 그런 라이브러리 지원도 부족한 언어를 쓰니 쉽지 않았다.

반응형

+ Recent posts