반응형

멀티 스테이지 빌드란?

멀티 스테이지 빌드는 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를 사용해서 간단히 풀 수 있을텐데 그런 라이브러리 지원도 부족한 언어를 쓰니 쉽지 않았다.

반응형
반응형

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

여러 문자열(이름)의 목록이 주어질 때, 이름의 앞부분과 다른 이름의 뒷부분이 같은 쌍의 갯수를 모두 찾는 문제이다.

 

기본적으로, 모든 이름 쌍에 대해 브루트포스로 앞뒤가 연결이 되는지 찾아내야 한다.

먼저 생각한 방법은, 한 단어의 모든 prefix를 구한 다음, 다른 단어가 그 prefix로 시작하는지 찾는 방법이었다.

이걸 단어의 차례만 바꿔서 한번 더 수행하면, 해당 쌍이 연결이 되는지 찾아낼 수 있다.

정답 코드 1

import sys


def check_connectable(a: str, b: str):
    for i in range(len(a) - 1, -1, -1):
        if b.startswith(a[i:]):
            return True
    for i in range(len(b) - 1, -1, -1):
        if a.startswith(b[i:]):
            return True
    return False


names = [name.rstrip() for name in sys.stdin.readlines()[1:]]
count = 0

for i in range(len(names) - 1):
    for j in range(i + 1, len(names)):
        if check_connectable(names[i], names[j]):
            count += 1

print(count)

check_connectable() 함수는 단어의 쌍에 대해 연결이 되는지 확인을 하는 함수임을 확인할 수 있다.

위 코드의 시간 복잡도는 O(n^2⋅m^2)다. n은 이름의 개수, 은 이름의 평균 길이다. n^2는 n개의 이름에 대해 2개씩 조합을 비교하는 부분이고, m^2check_connectable()의 문자열 슬라이싱과 startswith() 검사에 의한 부분이다. 이처럼 이중 루프와 문자열 비교가 중첩되어 성능이 떨어진다.

현재 check_connectable()에서는 단어별로 prefix를 매번 새롭게 문자열 슬라이싱으로 만들어내는 중복이 발생하고 있다. 이를 만회 하기 위해 아래의 방법을 생각해 볼 수 있겠다.

 

  • 이름별로 prefix와 postfix를 미리 추출하여 dict에 set 형태로 저장
    • 비교 대상 이름의 prefix와 postfix를 dict로 빠르게 조회 가능
  • 이름 쌍의 prefix set와 postfix set을 & 연산으로 빠르게 비교 연산

 

정답 코드 2 -  단어별 prefix와 postfix set를 만든뒤 단어쌍별 set 연산 수행

import sys


def get_prefix_suffix_sets(name: str):
    prefixes = {name[:i] for i in range(1, len(name) + 1)}
    suffixes = {name[i:] for i in range(len(name))}
    return prefixes, suffixes


names = [name.strip() for name in sys.stdin.readlines()[1:]]
prefix_set = {}
suffix_set = {}

for name in names:
    prefix_set[name], suffix_set[name] = get_prefix_suffix_sets(name)

count = 0

for i in range(len(names)):
    for j in range(i + 1, len(names)):
        if (
            suffix_set[names[i]] & prefix_set[names[j]]
            or suffix_set[names[j]] & prefix_set[names[i]]
        ):
            count += 1

print(count)

개선된 코드는 set 연산을 활용하여 문자열 비교 부분을 효율적으로 처리해 O(n2⋅m)로 성능을 크게 개선한다. n은 이름의 개수, 은 이름의 평균 길이다. 

시간복잡도를 구성하는 부분은 크게 둘로 나뉜다.

  • 1단계는 prefix와 postfix를 추출하는 부분으로, 단어 n개 에 대해 get_prefix_suffix_sets() 함수가 실행되는 부분이다. get_prefix_suffix_sets() 함수는 평균 길이가 m인 문자열에 대해 prefix와 postfix를 생성하므로, 전체 시간복잡도는 O(n*m)이다.
  • 2단계는 이름 쌍을 비교하는 부분이다. Set & 연산의 시간복잡도는 O(m)이다. n개의 이름에 대해 2개씩 조합을 비교하므로, 이 단계의 시간 복잡도는 O(n^2⋅m)이다..
    • 파이썬의 set 자료구조에서 & 연산은 평균적으로 O(min⁡(len(set1),len(set2)))의 시간 복잡도를 가진다. 이는 두 집합의 크기 중 작은 쪽을 기준으로 동작하기 때문이다.

위 두 단계를 종합하면, 전체 시간 복잡도는 O(n^2⋅m)이다. 이전 코드의 시간 복잡도인 O(n^2⋅m^2)보다 성능이 많이 개선된 것이다. 이 방식은 dict와 set 연산으로 효율성을 높이며, 특히 n이 큰 경우 효과적이다.

반응형
반응형

dataclass란?

Python 3.7에서 도입된 dataclass는 데이터를 저장하기 위한 클래스를 쉽게 생성할 수 있게 해주는 데코레이터이다. 보일러플레이트 코드를 줄이고 데이터 중심의 클래스를 더 효율적으로 작성할 수 있게 해준다.

전통적인 클래스에서는 __init__, __repr__, __eq__ 등의 특수 메서드를 직접 구현해야 했지만, dataclass를 사용하면 이러한 메서드들이 자동으로 생성된다.

 

1. 기본 사용법

클래스 정의 위에 @dataclass 데코레이터를 추가하고, 각 필드의 타입을 명시하면 완전한 기능을 갖춘 클래스가 생성된다.

from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    price: float
    in_stock: bool = True

# 사용 예시
book = Book("파이썬 완벽 가이드", "홍길동", 25000)
print(book)  # Book(title='파이썬 완벽 가이드', author='홍길동', price=25000, in_stock=True)

여기서 dataclass는 자동으로 다음을 생성한다:

  • __init__ 메서드: 객체 초기화를 위한 생성자
  • __repr__ 메서드: 객체의 문자열 표현을 위한 메서드
  • __eq__ 메서드: 객체 간 비교를 위한 메서드

 

2. 클래스 변수와 인스턴스 변수

dataclass에서는 클래스 변수와 인스턴스 변수의 선언 방식이 일반 클래스와 다르다. 

  • 인스턴스 변수 - 타입 어노테이션이 없음
  • 클래스 변수 - 1. 타입 어노테이션이 없거나 2. ClassVar[타입]이 명시되있음

기본적인 구분 방법

from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Student:
    # 인스턴스 변수 (타입 어노테이션 필수)
    name: str
    age: int
    
    # 클래스 변수 (타입 어노테이션 없음)
    school_name = "파이썬 고등학교"
    
    # 타입 힌트가 있는 클래스 변수
    MAX_AGE: ClassVar[int] = 20

 

3. 고급 기능

3.1 필드 옵션 설정

field() 함수를 사용하면 필드의 동작을 더 세밀하게 제어할 수 있다. default_factory를 사용하여 가변 객체의 기본값을 안전하게 설정하거나, init=False로 초기화에서 제외할 수 있다.

from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    scores: list[int] = field(default_factory=list)
    average: float = field(init=False)
    
    def __post_init__(self):
        self.average = sum(self.scores) / len(self.scores) if self.scores else 0

3.2 불변 데이터클래스 만들기

데이터의 불변성이 필요한 경우, frozen 매개변수를 사용하여 인스턴스의 속성을 변경할 수 없게 만들 수 있다. 이는 설정값이나 상수 데이터를 다룰 때 특히 유용하다.

@dataclass(frozen=True)
class Configuration:
    host: str
    port: int = 8080
    
config = Configuration("localhost")
# config.port = 9000  # 이 코드는 FrozenInstanceError를 발생시킨다

3.3 상속 활용하기

dataclass도 일반 클래스처럼 상속을 지원한다. 이를 통해 코드 재사용성을 높이고 계층적인 데이터 구조를 만들 수 있다.

@dataclass
class Vehicle:
    brand: str
    model: str
    year: int

@dataclass
class Car(Vehicle):
    doors: int = 4
    fuel_type: str = "gasoline"

 

4. Best Practice와 주의사항

4.1 타입 힌트 활용하기

dataclass는 타입 힌트와 함께 사용할 때 가장 효과적이다. IDE의 자동 완성과 타입 검사 기능을 최대한 활용할 수 있다.

from typing import Optional

@dataclass
class User:
    username: str
    email: str
    age: Optional[int] = None

4.2 기본값 설정 시 주의 사항(가변 객체 다루기)

dataclass에서 가변 객체(list, dict, set 등)를 다룰 때는 특별한 주의가 필요하다. 기본값으로 직접 가변 객체를 할당하면 모든 인스턴스가 같은 객체를 공유하게 되어 예기치 않은 버그가 발생할 수 있다. 이를 방지하기 위해 field(default_factory=)를 사용해야 한다.

# 잘못된 예시
@dataclass
class Wrong:
    items: list = []  # 모든 인스턴스가 같은 리스트를 공유

# 올바른 예시
@dataclass
class Right:
    items: list = field(default_factory=list)  # 각 인스턴스가 독립적인 리스트를 가짐

4.3 불변성 고려하기

데이터의 무결성이 중요한 경우 frozen=True 사용을 고려하는 것이 좋다. 실수로 인한 데이터 변경을 방지하고 함수형 프로그래밍 스타일을 지원할 수 있다.

4.4 클래스 변수와 인스턴스 변수 배치

코드의 가독성과 유지보수성을 높이기 위해서는 클래스 내의 변수들을 일관된 순서로 배치해야 한다. 인스턴스 변수를 먼저 선언하고, 그 다음에 클래스 변수를 선언하는 것이 권장된다.

@dataclass
class Example:
    # 1. 먼저 인스턴스 변수(필드)를 선언
    name: str
    value: int
    
    # 2. 그 다음 ClassVar를 사용한 클래스 변수
    VERSION: ClassVar[str] = "1.0"
    MAX_VALUE: ClassVar[int] = 100
    
    # 3. 마지막으로 일반 클래스 변수
    status = "active"

 

결론

dataclass는 데이터를 다루는 클래스를 작성할 때 매우 유용한 도구이다. 보일러플레이트 코드를 줄이고, 가독성을 높이며, 실수를 줄일 수 있다. 특히 데이터 모델링, API 응답 처리, 설정 관리 등에서 큰 효과를 발휘한다.

반응형
반응형

데이터 타입: Oracle과 SQL Server을 비교하면서 이해하기

데이터 타입에 대해 이해하기 위해, Oracle과 SQL Server의 데이터 타입을 비교해보겠다. 이 두 데이터베이스 시스템은 업계에서 가장 널리 사용되는 RDBMS이지만, 각각의 특성과 사용 방법에는 중요한 차이가 있다.

 

개요

데이터베이스 설계에서 가장 중요한 결정 중 하나는 적절한 데이터 타입의 선택이다. 잘못된 데이터 타입 선택은 성능 저하, 저장 공간 낭비, 데이터 정확성 손실 등의 문제를 일으킬 수 있다. Oracle과 SQL Server는 각각 고유한 데이터 타입 체계를 가지고 있다.

 

데이터 타입 비교표

1. 숫자형 데이터 타입

분류 Oracle SQL Server 설명
정수 NUMBER(p,0) INT 정수형 데이터 (-2,147,483,648 ~ 2,147,483,647)
십진수 NUMBER(p,s) DECIMAL/NUMERIC(p,s) 정밀도(p)와 스케일(s)을 지정할 수 있는 숫자형
부동소수점 FLOAT FLOAT/REAL 부동 소수점 숫자
이진 부동소수점 BINARY_FLOAT - 32비트 부동 소수점
  BINARY_DOUBLE - 64비트 부동 소수점
통화 - MONEY 통화 데이터

 

2. 문자형 데이터 타입

분류 Oracle SQL Server 설명
가변 길이 VARCHAR2(n) VARCHAR(n) Oracle: 최대 4000바이트
SQL Server: 최대 8000바이트
고정 길이 CHAR(n) CHAR(n) Oracle: 최대 2000바이트
유니코드 가변 NVARCHAR2(n) NVARCHAR(n) 유니코드 문자열 저장
대용량 텍스트 CLOB TEXT Oracle: 최대 4GB
SQL Server: 최대 2GB

 

3. 날짜/시간형 데이터 타입

분류 Oracle SQL Server 설명
날짜+시간 DATE DATETIME Oracle: 날짜와 시간 포함
SQL Server: 1753-01-01 ~ 9999-12-31
고정밀 날짜/시간 TIMESTAMP DATETIME2 더 높은 정밀도 제공
날짜만 - DATE 날짜 정보만 저장
시간만 - TIME 시간 정보만 저장
기간 INTERVAL - 시간 간격 저장

 

4. 이진 데이터 타입

분류 Oracle SQL Server 설명
대용량 이진 BLOB IMAGE 대용량 이진 데이터 저장
소용량 이진 RAW VARBINARY 작은 크기의 이진 데이터
파일스트림 - FILESTREAM 파일 시스템에 저장되는 이진 데이터

 

주요 차이점 상세 분석

1. 문자열 처리의 차이

Oracle의 VARCHAR2와 SQL Server의 VARCHAR는 비슷해 보이지만 중요한 차이가 있다. Oracle에서는 VARCHAR2를 사용하는 것이 권장되는데, 이는 향후 VARCHAR의 구현이 변경될 수 있기 때문이다. 또한, Oracle의 VARCHAR2는 실제 저장된 문자열의 길이만큼만 저장 공간을 사용하지만, SQL Server의 VARCHAR는 지정된 길이만큼의 공간을 미리 할당한다.

2. 숫자 데이터 처리

Oracle은 NUMBER 타입 하나로 대부분의 숫자 데이터를 처리할 수 있다. 반면 SQL Server는 더 세분화된 숫자 타입(TINYINT, SMALLINT, INT, BIGINT 등)을 제공한다. 이는 각각의 장단점이 있다:

  • Oracle의 접근방식: 단순하고 유연하지만 저장 공간 최적화가 어려울 수 있음
  • SQL Server의 접근방식: 더 세밀한 제어가 가능하고 저장 공간을 최적화할 수 있지만, 설계 시 더 많은 고려가 필요함

 

3. 날짜와 시간 처리

날짜와 시간 처리에서도 두 시스템은 큰 차이를 보인다:

  • Oracle의 DATE는 기본적으로 시간 정보를 포함
  • SQL Server는 DATE와 TIME을 별도로 제공하여 더 명확한 구분이 가능
  • SQL Server의 DATETIME2는 Oracle의 TIMESTAMP와 비슷한 정밀도를 제공

 

실무에서의 선택 기준

데이터 타입을 선택할 때는 다음 사항을 고려해야 한다:

  1. 데이터의 특성
    • 저장할 데이터의 크기와 형식
    • 필요한 정밀도 수준
    • 예상되는 데이터 증가량
  2. 성능 요구사항
    • 검색 속도
    • 저장 공간 효율성
    • 인덱싱 전략
  3. 애플리케이션 요구사항
    • 다른 시스템과의 호환성
    • 향후 마이그레이션 가능성
    • 개발 팀의 익숙도

 

결론

Oracle과 SQL Server는 각각의 장단점을 가지고 있다. Oracle은 단순하고 일관된 데이터 타입 체계를 제공하는 반면, SQL Server는 더 세분화되고 명시적인 데이터 타입을 제공한다.

데이터베이스 설계자는 프로젝트의 요구사항, 성능 목표, 그리고 팀의 경험을 고려하여 적절한 데이터 타입을 선택해야 한다. 특히 대규모 시스템에서는 초기의 데이터 타입 선택이 향후 시스템의 성능과 확장성에 큰 영향을 미칠 수 있음을 항상 명심해야 한다.

마지막으로, 데이터 타입 선택은 한 번의 결정으로 끝나는 것이 아니라, 시스템의 요구사항 변화에 따라 지속적으로 검토하고 최적화해야 하는 과정임을 기억해야 한다.

반응형

'Database > SQL' 카테고리의 다른 글

[SQL] SQL 명령어 종류  (0) 2025.01.27

+ Recent posts