ORM

Object-Relational Mapping(ORM)은 객체지향 프로그래밍 언어와 관계형 데이터베이스 사이의 ‘다리’ 역할을 하는 프로그래밍 기법이다.
Django ORM은 Python 클래스(모델)를 데이터베이스 테이블에 매핑하고, 모델 인스턴스를 테이블의 행으로 매핑한다. 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있게 된다.

Django ORM의 주요 목적은 다음과 같다:

Django ORM의 역사

Django ORM은 Django 웹 프레임워크의 일부로 2005년 처음 출시되었다. Lawrence Journal-World 신문사의 웹 개발팀이 빠른 웹 애플리케이션 개발을 위해 만들었으며, “프로젝트가 마감에 쫓기는 완벽주의자들을 위한 웹 프레임워크"라는 슬로건과 함께 시작되었다.

Django ORM은 초기부터 “batteries included” 철학을 채택하여, 데이터베이스 작업에 필요한 대부분의 기능을 기본적으로 제공했다. Django 버전이 업그레이드되면서 ORM도 지속적으로 개선되었으며, 다음과 같은 주요 발전이 있었다:

1.2 Django ORM의 핵심 구성요소

Django ORM은 다음과 같은 핵심 구성요소로 이루어져 있다:

  1. 모델(Model): 데이터베이스 테이블의 구조를 정의하는 Python 클래스이다. 각 모델 클래스는 하나의 데이터베이스 테이블에 대응된다.

  2. 매니저(Manager): 모델과 데이터베이스 간의 쿼리 인터페이스를 제공한다. 기본적으로 모든 모델은 objects라는 매니저를 가진다.

  3. 쿼리셋(QuerySet): 데이터베이스로부터 데이터를 검색하는 객체 컬렉션. 쿼리셋은 체이닝(chaining)을 통해 필터링, 정렬 등의 작업을 수행할 수 있다.

  4. 쿼리 빌더(Query Builder): 파이썬 코드로 작성된 쿼리를 SQL로 변환하는 시스템.

Django ORM의 동작 과정

Django ORM이 Python 코드를 SQL 쿼리로 변환하는 과정은 다음과 같다:

  1. 모델 정의: 개발자가 Django 모델 클래스를 정의한다.

    1
    2
    3
    4
    
    class Book(models.Model):
        title = models.CharField(max_length=100)
        author = models.ForeignKey(Author, on_delete=models.CASCADE)
        published_date = models.DateField()
    
  2. ORM 메서드 호출: 개발자가 쿼리셋 API를 사용하여 데이터베이스 작업을 정의한다.

    1
    
    recent_books = Book.objects.filter(published_date__year=2023).order_by('title')
    
  3. 쿼리 구성: Django ORM은 호출된 메서드들을 기반으로 내부적으로 쿼리 트리를 구성한다.

  4. SQL 변환: 실제로 데이터에 접근할 때(예: 반복, 슬라이싱, 평가 등), Django는 구성된 쿼리 트리를 데이터베이스별 SQL 방언으로 변환한다.

  5. 데이터베이스 실행: 생성된 SQL 쿼리가 데이터베이스에서 실행된다.

  6. 결과 매핑: 데이터베이스에서 반환된 결과가 Django 모델 인스턴스로 변환된다.

Django ORM의 특징

데이터베이스 추상화

Django ORM은 여러 관계형 데이터베이스 시스템(MySQL, PostgreSQL, SQLite, Oracle 등)에 대한 추상화 계층을 제공한다. 이를 통해 동일한 Python 코드로 다양한 데이터베이스 백엔드와 상호작용할 수 있다.

지연 실행(Lazy Evaluation)

Django의 쿼리셋은 지연 실행(lazy evaluation) 방식을 사용한다. 즉, 쿼리셋을 정의하는 시점에는 실제로 데이터베이스 쿼리가 실행되지 않고, 결과가 실제로 필요한 시점(예: 반복, 슬라이싱, 캐싱)에 쿼리가 실행된다. 이는 불필요한 데이터베이스 접근을 줄이고 효율성을 높이는 데 기여한다.

1
2
3
4
5
6
# 이 시점에서는 쿼리가 실행되지 않음
queryset = Book.objects.filter(published_date__year=2023)

# 실제로 데이터에 접근할 때 쿼리가 실행됨
for book in queryset:
    print(book.title)

체이닝 API

Django ORM은 메서드 체이닝을 통해 복잡한 쿼리를 직관적으로 구성할 수 있는 API를 제공한다.

1
2
3
4
5
6
7
queryset = Book.objects.filter(
    author__name='Jane Austen'
).exclude(
    published_date__year__lt=2000
).order_by(
    '-published_date'
)[:5]

모델 관계 표현

Django ORM은 관계형 데이터베이스의 다양한 관계(일대일, 일대다, 다대다)를 Python 객체 간의 관계로 자연스럽게 표현할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    
# 저자의 모든 책 접근
author = Author.objects.get(id=1)
author_books = author.books.all()

# 책의 저자 접근
book = Book.objects.get(id=1)
books_author = book.author

Migration 시스템

Django ORM은 강력한 마이그레이션 시스템을 제공하여 모델 변경사항을 데이터베이스 스키마에 적용할 수 있다. 이는 데이터베이스 스키마 버전 관리와 팀 협업을 용이하게 한다.

1
2
python manage.py makemigrations  # 모델 변경사항을 감지하여 마이그레이션 파일 생성
python manage.py migrate         # 마이그레이션을 데이터베이스에 적용

Django 모델 정의와 필드 타입

모델 정의 기초

Django에서 모델은 django.db.models.Model을 상속받는 Python 클래스로 정의된다.
각 모델 클래스는 데이터베이스의 하나의 테이블에 대응된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    birth_date = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return self.name
    
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    is_bestseller = models.BooleanField(default=False)
    
    def __str__(self):
        return self.title

모델을 정의하면 Django는 다음을 자동으로 생성한다:

필드 타입과 옵션

Django ORM은 다양한 데이터 타입을 표현하기 위한 여러 필드 타입을 제공한다.

문자열 필드
숫자 필드
날짜/시간 필드
바이너리/미디어 필드
불리언/선택 필드
관계 필드
자주 사용되는 필드 옵션

모델 메타데이터 (Meta 클래스)

모델의 Meta 내부 클래스를 통해 모델의 동작과 메타데이터를 정의할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    
    class Meta:
        db_table = 'bookstore_books'  # 테이블 이름 지정
        ordering = ['-published_date', 'title']  # 정렬 순서 지정
        verbose_name = 'Book'  # 단수 이름
        verbose_name_plural = 'Books'  # 복수 이름
        unique_together = [['title', 'author']]  # 복합 유니크 제약조건
        indexes = [  # 인덱스 정의
            models.Index(fields=['title']),
            models.Index(fields=['author', 'published_date']),
        ]
        constraints = [  # 데이터베이스 제약조건
            models.CheckConstraint(
                check=models.Q(price__gt=0),
                name='positive_price'
            )
        ]
        abstract = False  # 추상 모델 여부
        app_label = 'bookstore'  # 앱 레이블 지정

모델 관계 설정

일대다(One-to-Many) 관계

일대다 관계는 하나의 레코드가 다른 테이블의 여러 레코드와 연결될 수 있는 관계이다.
Django에서는 ForeignKey를 사용하여 구현한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Author(models.Model):
    name = models.CharField(max_length=100)
    
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )

이 예시에서 각 책은 한 명의 저자에게 속하고, 한 저자는 여러 책을 가질 수 있다.

on_delete 옵션은 참조된 객체가 삭제될 때 수행할 동작을 지정한다:

related_name은 역참조할 때 사용할 이름을 지정한다.
이를 통해 저자 객체에서 해당 저자의 모든 책에 접근할 수 있다:

1
2
author = Author.objects.get(id=1)
author_books = author.books.all()  # related_name='books'를 사용

다대다(Many-to-Many) 관계

다대다 관계는 한 테이블의 여러 레코드가 다른 테이블의 여러 레코드와 연결될 수 있는 관계이다.
Django에서는 ManyToManyField를 사용하여 구현한다.

1
2
3
4
5
6
class Tag(models.Model):
    name = models.CharField(max_length=50)
    
class Book(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag, related_name='books')

이 예시에서 한 책은 여러 태그를 가질 수 있고, 한 태그는 여러 책에 적용될 수 있다.

태그를 추가하고 조회하는 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
book = Book.objects.get(id=1)
tag = Tag.objects.get(name='Fantasy')

# 태그 추가
book.tags.add(tag)

# 여러 태그 추가
book.tags.add(tag1, tag2, tag3)

# 태그 제거
book.tags.remove(tag)

# 태그 목록 설정 (기존 관계 모두 삭제 후 지정된 태그만 설정)
book.tags.set([tag1, tag2])

# 모든 태그 제거
book.tags.clear()

# 태그가 있는지 확인
if book.tags.filter(id=tag.id).exists():
    print("이 책에는 해당 태그가 있습니다.")

# 역참조: 특정 태그가 적용된 모든 책 조회
fantasy_books = Tag.objects.get(name='Fantasy').books.all()
중간 모델 사용 (through 옵션)

때로는 다대다 관계에 추가 데이터를 저장해야 할 수 있다.
이런 경우 through 옵션을 사용하여 명시적인 중간 모델을 지정할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Student(models.Model):
    name = models.CharField(max_length=100)
    
class Course(models.Model):
    name = models.CharField(max_length=100)
    students = models.ManyToManyField(
        Student,
        through='Enrollment',
        related_name='courses'
    )
    
class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    date_enrolled = models.DateField(auto_now_add=True)
    grade = models.CharField(max_length=2, blank=True)

중간 모델을 사용할 때의 작업 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 등록 생성 (관계 설정)
student = Student.objects.get(id=1)
course = Course.objects.get(id=1)
enrollment = Enrollment.objects.create(student=student, course=course)

# 또는
enrollment = Enrollment(student=student, course=course, grade='A')
enrollment.save()

# 특정 학생이 등록한 모든 과정 조회
student_courses = student.courses.all()

# 특정 과정을 수강하는 모든 학생 조회
course_students = course.students.all()

# 중간 모델을 통해 추가 정보 조회
enrollments = Enrollment.objects.filter(student=student)
for enrollment in enrollments:
    print(f"Course: {enrollment.course.name}, Grade: {enrollment.grade}")

일대일(One-to-One) 관계

일대일 관계는 한 테이블의 레코드가 다른 테이블의 단 하나의 레코드와만 연결되는 관계이다.
Django에서는 OneToOneField를 사용하여 구현한다.

1
2
3
4
5
6
7
class User(models.Model):
    username = models.CharField(max_length=100)
    
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    bio = models.TextField()
    birth_date = models.DateField(null=True, blank=True)

이 예시에서 각 사용자는 하나의 프로필만 가질 수 있고, 각 프로필은 하나의 사용자에게만 속한다.

일대일 관계 작업 방법:

1
2
3
4
5
6
7
8
9
# 프로필 생성
user = User.objects.create(username='johndoe')
profile = Profile.objects.create(user=user, bio='Python developer')

# 사용자에서 프로필 접근
user_profile = user.profile  # 직접 접근 가능 (related_name='profile')

# 프로필에서 사용자 접근
profile_user = profile.user

자기참조 관계

모델이 자기 자신을 참조하는 관계를 정의할 수도 있다:

1
2
3
4
5
6
7
8
9
class Employee(models.Model):
    name = models.CharField(max_length=100)
    supervisor = models.ForeignKey(
        'self',  # 자기 자신을 참조
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='subordinates'
    )

이 예시에서 각 직원은 한 명의 상사를 가질 수 있고, 한 상사는 여러 부하 직원을 가질 수 있다.

자기참조 관계 작업 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 상사 생성
boss = Employee.objects.create(name='Boss')

# 부하 직원 생성
employee1 = Employee.objects.create(name='Employee 1', supervisor=boss)
employee2 = Employee.objects.create(name='Employee 2', supervisor=boss)

# 상사의 모든 부하 직원 조회
subordinates = boss.subordinates.all()  # [employee1, employee2]

# 직원의 상사 조회
supervisor = employee1.supervisor  # boss

Generic Relations

Django의 ContentType 프레임워크를 사용하면 특정 모델 유형에 국한되지 않는 일반적인 관계를 정의할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

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

class TaggedItem(models.Model):
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    
    # 다음 세 필드가 Generic Foreign Key를 구성
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

이를 통해 어떤 모델 타입에도 태그를 적용할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.contrib.contenttypes.models import ContentType

# 책에 태그 추가
book = Book.objects.get(id=1)
tag = Tag.objects.get(name='Fantasy')

TaggedItem.objects.create(
    tag=tag,
    content_type=ContentType.objects.get_for_model(Book),
    object_id=book.id
)

# 특정 객체에 적용된 모든 태그 조회
tagged_items = TaggedItem.objects.filter(
    content_type=ContentType.objects.get_for_model(Book),
    object_id=book.id
)

QuerySet API와 쿼리 작업

QuerySet은 데이터베이스에서 가져올 수 있는 객체 모음을 나타내는 Django 클래스이다.
이는 Django ORM의 핵심 요소로, 데이터베이스로부터 데이터를 검색, 필터링, 정렬하는 등의 작업을 수행하는 인터페이스를 제공한다.

QuerySet의 주요 특징

지연 평가(Lazy Evaluation)

QuerySet은 정의되는 시점이 아닌 결과가 실제로 필요한 시점에 데이터베이스 쿼리를 실행한다. 다음 상황에서 QuerySet이 평가(evaluate)된다:

캐싱 메커니즘

QuerySet은 내부적으로 캐싱 메커니즘을 가지고 있다. 한 번 실행된 QuerySet의 결과는 메모리에 캐시되어, 같은 QuerySet에 다시 접근할 때 데이터베이스 쿼리를 다시 실행하지 않는다.

1
2
3
queryset = Book.objects.all()
print([book.title for book in queryset])  # 첫 번째 실행: 데이터베이스 쿼리 실행
print([book.title for book in queryset])  # 두 번째 실행: 캐시된 결과 사용

그러나 QuerySet을 새로 정의하면 캐시가 공유되지 않는다:

1
2
3
4
5
queryset1 = Book.objects.all()
print([book.title for book in queryset1])  # 데이터베이스 쿼리 실행

queryset2 = Book.objects.all()
print([book.title for book in queryset2])  # 다시 데이터베이스 쿼리 실행
체이닝 가능한 API

QuerySet API는 체이닝이 가능하도록 설계되어 있어, 복잡한 쿼리를 직관적으로 표현할 수 있다. 대부분의 QuerySet 메서드는 새로운 QuerySet을 반환하므로, 여러 메서드를 연속적으로 호출할 수 있다.

1
2
3
4
5
6
7
8
9
Book.objects.filter(
    published_date__year=2023
).exclude(
    genre='fantasy'
).filter(
    author__country='UK'
).order_by(
    'title'
)
쿼리셋 클론

QuerySet 메서드는 원본 QuerySet을 변경하지 않고 새로운 QuerySet을 반환한다. 이는 기존 쿼리셋을 기반으로 다양한 변형을 만들 수 있게 해준다.

1
2
3
base_queryset = Book.objects.all()
fantasy_books = base_queryset.filter(genre='fantasy')
recent_books = base_queryset.filter(published_date__year=2023)

QuerySet의 내부 구조

QuerySet의 내부 구조를 이해하면 더 효율적인 쿼리를 작성하는 데 도움이 된다:

쿼리(Query) 객체

QuerySet은 내부적으로 django.db.models.sql.Query 클래스의 인스턴스를 포함하고 있다. 이 Query 객체는 SQL 쿼리를 생성하는 데 필요한 모든 정보(테이블, 조건, 정렬 등)를 담고 있다.

1
2
queryset = Book.objects.filter(published_date__year=2023)
print(queryset.query)  # SQL 쿼리 출력
쿼리 트리(Query Tree)

복잡한 필터링 조건은 내부적으로 쿼리 트리(Q 객체 트리)로 변환된다. 이 트리는 AND, OR, NOT과 같은 논리 연산자와 필터 조건을 조합한 구조이다.

1
2
3
4
5
6
from django.db.models import Q

queryset = Book.objects.filter(
    Q(published_date__year=2023) | 
    (Q(author__name='Jane Austen') & ~Q(genre='romance'))
)
결과 캐시(Result Cache)

QuerySet은 결과를 캐싱하기 위한 _result_cache 속성을 가지고 있다. 처음 QuerySet이 평가될 때 이 캐시에 결과가 저장되며, 이후 같은 QuerySet에 접근할 때는 이 캐시를 사용한다.

QuerySet 객체 다루기

객체 생성
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 방법 1: 인스턴스 생성 후 저장
author = Author(name='J.K. Rowling', birth_date='1965-07-31')
author.save()

# 방법 2: create() 메서드 사용
author = Author.objects.create(name='George Orwell', birth_date='1903-06-25')

# 방법 3: get_or_create() 메서드 - 존재하면 가져오고, 없으면 생성
author, created = Author.objects.get_or_create(
    name='Ernest Hemingway',
    defaults={'birth_date': '1899-07-21'}
)
객체 수정
1
2
3
4
5
6
7
# 방법 1: 인스턴스 속성 변경 후 저장
author = Author.objects.get(id=1)
author.name = '제이케이 롤링'
author.save()

# 방법 2: update() 메서드 사용 (여러 객체 동시 수정 가능)
Author.objects.filter(birth_date__year__lt=1900).update(is_classic=True)
기본 조회 메서드
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 모든 객체 가져오기
all_authors = Author.objects.all()

# 특정 ID로 객체 가져오기
author = Author.objects.get(id=1)

# 조건에 맞는 첫 번째 객체 가져오기
first_author = Author.objects.filter(name__contains='J.K.').first()

# 특정 ID 목록의 객체 가져오기
specific_authors = Author.objects.filter(id__in=[1, 3, 5])
필터링 메서드와 연산자
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# filter(): 조건에 맞는 객체 반환
recent_books = Book.objects.filter(published_date__year=2023)

# exclude(): 조건에 맞지 않는 객체 반환
old_books = Book.objects.exclude(published_date__year__gte=2000)

# 필드 조회 연산자
books = Book.objects.filter(
    title__contains='Harry',  # 부분 문자열 포함
    published_date__year__gte=2000,  # 연도가 2000 이상
    price__lt=20.00,  # 가격이 20 미만
    author__name__in=['J.K. Rowling', 'George Orwell']  # 지정된 목록 내 포함
)

# 대소문자 구분 없는 조회
books = Book.objects.filter(title__icontains='harry')

# 범위 조회
books = Book.objects.filter(published_date__range=('2020-01-01', '2020-12-31'))

# 정규식 조회
books = Book.objects.filter(title__regex=r'^[A-Z]')
Q 객체를 사용한 복잡한 조건

Q 객체를 사용하면 OR, AND, NOT 등의 복잡한 조건을 구성할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.db.models import Q

# OR 조건
books = Book.objects.filter(
    Q(author__name='J.K. Rowling') | Q(author__name='George Orwell')
)

# AND 조건
books = Book.objects.filter(
    Q(published_date__year=2020) & Q(price__lt=20.00)
)

# NOT 조건
books = Book.objects.filter(~Q(author__name='J.K. Rowling'))

# 복잡한 중첩 조건
books = Book.objects.filter(
    (Q(author__name='J.K. Rowling') | Q(author__name='George Orwell')) &
    ~Q(published_date__year__lt=2000)
)
객체 정렬
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 단일 필드로 정렬
books = Book.objects.order_by('title')

# 여러 필드로 정렬 (내림차순은 필드 앞에 - 추가)
books = Book.objects.order_by('-published_date', 'title')

# random() 함수를 사용한 랜덤 정렬
books = Book.objects.order_by('?')

# 기본 정렬 순서 지정 (모델의 Meta 클래스에서)
class Book(models.Model):
    # 필드 정의…
    
    class Meta:
        ordering = ['-published_date', 'title']

객체 삭제

1
2
3
4
5
6
# 단일 객체 삭제
author = Author.objects.get(id=1)
author.delete()

# 여러 객체 삭제
Author.objects.filter(birth_date__year__lt=1900).delete()

집계 및 주석

Django ORM은 데이터베이스 수준의 집계 및 주석 기능을 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db.models import Count, Avg, Sum, Min, Max, F, ExpressionWrapper, DecimalField

# 집계 함수: 모든 도서의 평균 가격, 총 도서 수 등 계산
stats = Book.objects.aggregate(
    total_books=Count('id'),
    avg_price=Avg('price'),
    max_price=Max('price'),
    min_price=Min('price'),
    total_value=Sum('price')
)

# 주석 추가: 각 저자의 도서 수, 평균 가격 등 계산
authors = Author.objects.annotate(
    book_count=Count('book'),
    avg_book_price=Avg('book__price')
)

# 계산된 필드
books = Book.objects.annotate(
    discount_price=F('price') * 0.9,
    profit=ExpressionWrapper(
        F('price') - F('cost'),
        output_field=DecimalField(max_digits=10, decimal_places=2)
    )
)

그룹화 및 값 검색

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 그룹화하여 집계
genre_stats = Book.objects.values('genre').annotate(
    count=Count('id'),
    avg_price=Avg('price')
).order_by('-count')

# values(): 딕셔너리 형태로 특정 필드만 가져오기
books_data = Book.objects.values('id', 'title', 'author__name')
# [{'id': 1, 'title': 'Book 1', 'author__name': 'Author 1'}, …]

# values_list(): 튜플 형태로 특정 필드만 가져오기
book_titles = Book.objects.values_list('title', flat=True)  # flat=True는 단일 필드일 때 사용 가능
# ['Book 1', 'Book 2', …]

# 중첩 관계 그룹화: 연도별, 장르별 통계
yearly_genre_stats = Book.objects.values(
    'published_date__year', 'genre'
).annotate(
    count=Count('id'),
    avg_price=Avg('price')
).order_by('published_date__year', 'genre')

쿼리 최적화 기법

Django ORM에서 가장 흔한 성능 이슈 중 하나는 “N+1 쿼리 문제"이다.
이는 관련 객체를 로드할 때 발생하며, Django는 이 문제를 해결하기 위한 두 가지 메서드를 제공한다.

select_related는 SQL JOIN을 사용하여 관련 객체를 함께 가져온다.
주로 ForeignKey와 같은 일대일(one-to-one) 또는 다대일(many-to-one) 관계에 사용된다.

1
2
3
4
5
6
7
8
9
# N+1 쿼리 문제 (비효율적인 방법)
books = Book.objects.all()  # 1개의 쿼리
for book in books:
    print(book.author.name)  # 각 책마다 추가 쿼리 발생 (N개의 쿼리)

# select_related 사용 (효율적인 방법)
books = Book.objects.select_related('author').all()  # 1개의 쿼리 (JOIN 사용)
for book in books:
    print(book.author.name)  # 추가 쿼리 없음

여러 관계를 함께 로드할 수도 있다:

1
2
3
4
5
# 다중 관계 로드
books = Book.objects.select_related('author', 'publisher').all()

# 중첩 관계 로드
books = Book.objects.select_related('author__hometown').all()

prefetch_related는 별도의 쿼리를 실행하여 관련 객체를 가져온 후, Python에서 결합한다.
주로 ManyToManyField나 역방향 ForeignKey와 같은 일대다(one-to-many) 또는 다대다(many-to-many) 관계에 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# N+1 쿼리 문제 (비효율적인 방법)
authors = Author.objects.all()  # 1개의 쿼리
for author in authors:
    books = author.books.all()  # 각 저자마다 추가 쿼리 발생 (N개의 쿼리)
    print(f"{author.name}: {', '.join(book.title for book in books)}")

# prefetch_related 사용 (효율적인 방법)
authors = Author.objects.prefetch_related('books').all()  # 2개의 쿼리
for author in authors:
    books = author.books.all()  # 추가 쿼리 없음 (이미 프리페치됨)
    print(f"{author.name}: {', '.join(book.title for book in books)}")

여러 관계를 함께 프리페치할 수도 있다:

1
2
3
4
5
6
7
8
# 여러 관계 프리페치
authors = Author.objects.prefetch_related('books', 'awards').all()

# 중첩 관계 프리페치
books = Book.objects.prefetch_related('author__awards').all()

# select_related와 prefetch_related 함께 사용
books = Book.objects.select_related('author').prefetch_related('tags', 'reviews').all()

Prefetch 객체

Prefetch 객체를 사용하면 더 복잡한 프리페치 작업을 수행할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.db.models import Prefetch

# 특정 조건의 관련 객체만 프리페치
authors = Author.objects.prefetch_related(
    Prefetch('books', queryset=Book.objects.filter(published_date__year=2023))
).all()

# 프리페치된 결과를 다른 속성 이름으로 저장
authors = Author.objects.prefetch_related(
    Prefetch('books', queryset=Book.objects.filter(is_bestseller=True), to_attr='bestsellers')
).all()

for author in authors:
    # author.bestsellers는 이미 필터링된 리스트
    print(f"{author.name}의 베스트셀러: {len(author.bestsellers)}권")

Only와 Defer

성능을 최적화하기 위해 특정 필드만 가져오거나 특정 필드를 제외할 수 있다:

only()

only()는 지정된 필드만 데이터베이스에서 가져옵니다. 다른 필드가 접근되면 추가 쿼리가 발생할 수 있다:

1
2
3
4
5
# 특정 필드만 가져오기
books = Book.objects.only('id', 'title', 'published_date').all()

# 중첩 관계의 필드 지정
books = Book.objects.select_related('author').only('id', 'title', 'author__name').all()
defer()

defer()는 지정된 필드를 제외하고 모든 필드를 가져온다:

1
2
3
4
5
# 특정 필드 제외하기 (큰 텍스트 필드 등)
books = Book.objects.defer('description', 'content').all()

# 여러 번 호출하여 필드 추가 제외
books = Book.objects.defer('description').defer('content').all()

iterator()를 사용한 대용량 데이터 처리

대량의 데이터를 처리할 때는 iterator()를 사용하면 메모리 사용량을 최소화할 수 있다:

1
2
3
# 대용량 데이터 순회 (메모리 효율적)
for book in Book.objects.iterator():
    process_book(book)

데이터베이스 함수 및 표현식

Django ORM은 데이터베이스 수준에서 연산을 수행할 수 있는 다양한 함수와 표현식을 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from django.db.models.functions import Concat, Lower, Upper, Length, Substr, Extract
from django.db.models import Value, F, Func, ExpressionWrapper, DecimalField, CharField

# 문자열 함수
authors = Author.objects.annotate(
    full_name=Concat('first_name', Value(' '), 'last_name', output_field=CharField()),
    name_length=Length('name')
)

# 날짜 함수
books_by_month = Book.objects.annotate(
    month=Extract('published_date', 'month')
).values('month').annotate(
    count=Count('id')
).order_by('month')

# F() 표현식: 데이터베이스 수준에서 필드 참조
Book.objects.update(discount_price=F('price') * 0.9)

# 사용자 정의 함수
class Round(Func):
    function = 'ROUND'
    arity = 2

Book.objects.annotate(
    rounded_price=Round('price', Value(0))
)

일괄 생성 및 업데이트

다수의 객체를 생성하거나 업데이트할 때는 bulk 메서드를 사용하면 효율적이다:

bulk_create()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 비효율적인 방법 (N개의 INSERT 쿼리)
for i in range(1000):
    Book.objects.create(title=f'Book {i}', author=author, price=10.99)

# 효율적인 방법 (소수의 쿼리)
books = [
    Book(title=f'Book {i}', author=author, price=10.99)
    for i in range(1000)
]
Book.objects.bulk_create(books, batch_size=100)  # batch_size로 쿼리 분할
bulk_update()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 비효율적인 방법 (N개의 UPDATE 쿼리)
books = Book.objects.filter(genre='fantasy')
for book in books:
    book.price *= 1.1  # 10% 가격 인상
    book.save()

# 효율적인 방법 (소수의 쿼리)
books = list(Book.objects.filter(genre='fantasy'))
for book in books:
    book.price *= 1.1

Book.objects.bulk_update(books, ['price'], batch_size=100)

트랜잭션과 고급 기능

트랜잭션 관리

Django는 데이터베이스 트랜잭션을 관리하는 여러 방법을 제공한다:

Atomic 데코레이터 및 컨텍스트 매니저
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.db import transaction

# 데코레이터로 사용
@transaction.atomic
def transfer_funds(from_account, to_account, amount):
    from_account.balance -= amount
    from_account.save()
    
    # 오류가 발생하면 모든 변경사항이 롤백됨
    if amount > 100000:
        raise ValueError("송금 한도 초과")
    
    to_account.balance += amount
    to_account.save()

# 컨텍스트 매니저로 사용
def transfer_funds(from_account, to_account, amount):
    with transaction.atomic():
        from_account.balance -= amount
        from_account.save()
        
        if amount > 100000:
            raise ValueError("송금 한도 초과")
        
        to_account.balance += amount
        to_account.save()
세이브포인트

트랜잭션 내에서 부분적으로 커밋 또는 롤백할 수 있는 세이브포인트를 사용할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.db import transaction

def complex_operation():
    with transaction.atomic():
        # 첫 번째 작업
        operation_1()
        
        # 세이브포인트 설정
        sid = transaction.savepoint()
        
        try:
            # 두 번째 작업 (실패할 수 있음)
            operation_2()
        except:
            # 두 번째 작업만 롤백
            transaction.savepoint_rollback(sid)
            # 대체 작업 수행
            alternative_operation()
        else:
            # 세이브포인트 커밋
            transaction.savepoint_commit(sid)
        
        # 세 번째 작업
        operation_3()

락킹 및 동시성 제어

Django ORM은 데이터베이스 수준의 락킹을 지원한다:

select_for_update()
1
2
3
4
5
6
7
from django.db import transaction

with transaction.atomic():
    # 락을 획득하여 다른 트랜잭션이 이 레코드를 수정하지 못하게 함
    account = Account.objects.select_for_update().get(id=account_id)
    account.balance -= amount
    account.save()

락 획득 시 타임아웃 지정:

1
2
3
4
5
6
7
8
9
from django.db import transaction

with transaction.atomic():
    try:
        # 3초 내에 락 획득 시도
        account = Account.objects.select_for_update(nowait=False, skip_locked=False, of=('self',)).get(id=account_id)
    except DatabaseError:
        # 락 획득 실패
        return "현재 다른 작업이 진행 중입니다. 잠시 후 다시 시도하세요."

로우 레벨 SQL 실행

때로는 Django ORM으로 표현하기 어려운 복잡한 쿼리를 직접 SQL로 실행해야 할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from django.db import connection

def complex_query(min_price, max_price):
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT b.title, a.name, b.price
            FROM bookstore_book b
            JOIN bookstore_author a ON b.author_id = a.id
            WHERE b.price BETWEEN %s AND %s
            ORDER BY b.price DESC
        """, [min_price, max_price])
        
        return cursor.fetchall()  # 튜플 목록 반환

# 명명된 매개변수 사용 (PostgreSQL)
def complex_query_postgres(min_price, max_price):
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT b.title, a.name, b.price
            FROM bookstore_book b
            JOIN bookstore_author a ON b.author_id = a.id
            WHERE b.price BETWEEN %(min)s AND %(max)s
            ORDER BY b.price DESC
        """, {'min': min_price, 'max': max_price})
        
        columns = [col[0] for col in cursor.description]
        return [dict(zip(columns, row)) for row in cursor.fetchall()]  # 딕셔너리 목록 반환

커스텀 매니저 및 쿼리셋

자주 사용하는 쿼리 로직을 캡슐화하기 위해 커스텀 매니저와 쿼리셋을 정의할 수 있다:

커스텀 매니저

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class BookManager(models.Manager):
    def get_queryset(self):
        # 기본 쿼리셋 수정 (예: 항상 활성 상태인 책만 반환)
        return super().get_queryset().filter(is_active=True)
    
    def bestsellers(self):
        # 베스트셀러만 반환하는 메서드
        return self.get_queryset().filter(is_bestseller=True)
    
    def by_genre(self, genre):
        # 특정 장르의 책만 반환하는 메서드
        return self.get_queryset().filter(genre=genre)

class Book(models.Model):
    title = models.CharField(max_length=200)
    # 기타 필드…
    is_active = models.BooleanField(default=True)
    is_bestseller = models.BooleanField(default=False)
    
    objects = BookManager()  # 기본 매니저 교체
    all_books = models.Manager()  # 모든 책에 접근하기 위한 추가 매니저

사용 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 활성 상태인 책만 반환
active_books = Book.objects.all()

# 모든 책 반환 (활성 상태와 관계없이)
all_books = Book.all_books.all()

# 베스트셀러만 반환
bestsellers = Book.objects.bestsellers()

# 특정 장르의 책만 반환
fantasy_books = Book.objects.by_genre('fantasy')

커스텀 쿼리셋

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BookQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)
    
    def bestsellers(self):
        return self.filter(is_bestseller=True)
    
    def by_genre(self, genre):
        return self.filter(genre=genre)
    
    def published_after(self, date):
        return self.filter(published_date__gte=date)

class BookManager(models.Manager):
    def get_queryset(self):
        return BookQuerySet(self.model, using=self._db)
    
    def active(self):
        return self.get_queryset().active()
    
    def bestsellers(self):
        return self.get_queryset().bestsellers()
    
    def by_genre(self, genre):
        return self.get_queryset().by_genre(genre)

class Book(models.Model):
    # 필드 정의…
    
    objects = BookManager()

커스텀 쿼리셋의 장점은 체이닝이 가능하다는 것이다:

1
2
# 여러 조건 체이닝
recent_fantasy_bestsellers = Book.objects.bestsellers().by_genre('fantasy').published_after('2022-01-01')

모델 상속과 추상화

Django ORM은 세 가지 유형의 모델 상속을 지원한다:

추상 기본 클래스 상속

추상 기본 클래스는 여러 모델에서 공통으로 사용되는 필드를 정의하는 데 유용하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True  # 이 모델은 테이블을 생성하지 않습니다
        
class Book(TimeStampedModel):
    title = models.CharField(max_length=200)
    # 기타 필드…
    
    # Book 모델은 title, created_at, updated_at 필드를 갖습니다

추상 기본 클래스의 특징:

다중 테이블 상속

다중 테이블 상속은 각 모델마다 별도의 테이블을 생성하고 자동으로 일대일 관계를 설정한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    description = models.TextField()
    
class Book(Product):
    author = models.CharField(max_length=100)
    isbn = models.CharField(max_length=13)
    published_date = models.DateField()
    
    # Book 테이블은 id, author, isbn, published_date 필드를 갖고,
    # Product 테이블과 일대일 관계를 가집니다

다중 테이블 상속의 특징:

프록시 모델 상속

프록시 모델은 원본 모델의 행동만 변경하고 새로운 테이블을 생성하지 않는다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Person(models.Model):
    name = models.CharField(max_length=100)
    birth_date = models.DateField()
    
    def get_age(self):
        from datetime import date
        return date.today().year - self.birth_date.year
    
class Student(Person):
    class Meta:
        proxy = True  # 새로운 테이블 생성 안 함
        
    def is_eligible_for_scholarship(self):
        # 학생 특화 메서드
        return self.get_age() < 25
    
    @classmethod
    def honor_roll(cls):
        # 학생 특화 쿼리셋 메서드
        return cls.objects.filter(gpa__gte=3.5)

프록시 모델의 특징:

신호(Signal) 시스템

Django의 신호 시스템은 모델 인스턴스의 생명주기 동안 발생하는 이벤트를 처리하기 위한 메커니즘을 제공한다.

내장 신호

Django는 다음과 같은 내장 신호를 제공한다:

신호 사용 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    """사용자가 생성될 때 프로필도 함께 생성"""
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    """사용자가 저장될 때 프로필도 함께 저장"""
    instance.profile.save()

@receiver(pre_delete, sender=Book)
def handle_book_deletion(sender, instance, **kwargs):
    """책이 삭제되기 전에 로그 기록"""
    from .models import DeletionLog
    DeletionLog.objects.create(
        model_name='Book',
        object_id=instance.id,
        description=f"Book '{instance.title}' was deleted"
    )

커스텀 신호

필요에 따라 커스텀 신호를 정의할 수도 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# signals.py
from django.dispatch import Signal

# 커스텀 신호 정의
payment_completed = Signal()  # providing_args=['order', 'amount']

# 모델에서 신호 발생
def process_payment(self, amount):
    # 결제 처리 로직…
    if payment_successful:
        from .signals import payment_completed
        payment_completed.send(sender=self.__class__, order=self, amount=amount)

# 신호 수신
@receiver(payment_completed)
def handle_payment_completed(sender, order, amount, **kwargs):
    # 결제 완료 처리 로직…
    order.status = 'paid'
    order.save()
    
    # 이메일 발송
    send_payment_confirmation_email(order, amount)

신호 vs. 오버라이딩

신호는 편리하지만, 다음과 같은 경우에는 메서드 오버라이딩이 더 적합할 수 있다:

메서드 오버라이딩 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Book(models.Model):
    title = models.CharField(max_length=200)
    # 기타 필드…
    
    def save(self, *args, **kwargs):
        # 저장 전 수행할 작업
        self.title = self.title.strip()  # 앞뒤 공백 제거
        
        # 실제 저장 수행
        super().save(*args, **kwargs)
        
        # 저장 후 수행할 작업
        self.update_search_index()
    
    def delete(self, *args, **kwargs):
        # 삭제 전 수행할 작업
        self.log_deletion()
        
        # 실제 삭제 수행
        super().delete(*args, **kwargs)

미들웨어와 데이터베이스 라우팅

데이터베이스 라우팅

Django는 여러 데이터베이스를 사용할 수 있으며, 쿼리를 적절한 데이터베이스로 라우팅할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'main_db',
        # 기타 설정…
    },
    'replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'replica_db',
        # 기타 설정…
    },
    'archive': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'archive_db',
        # 기타 설정…
    }
}

DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.ArchiveRouter']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# routers.py
class AuthRouter:
    """사용자 인증 관련 모델을 기본 데이터베이스로 라우팅"""
    
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'auth':
            return 'default'
        return None
    
    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'auth':
            return 'default'
        return None
    
    def allow_relation(self, obj1, obj2, **hints):
        if (obj1._meta.app_label == 'auth' or
            obj2._meta.app_label == 'auth'):
            return True
        return None
    
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'auth':
            return db == 'default'
        return None

class BookRouter:
    """Book 관련 쿼리를 읽기/쓰기 분리"""
    
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'bookstore':
            return 'replica'  # 읽기는 복제본 데이터베이스로
        return None
    
    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'bookstore':
            return 'default'  # 쓰기는 기본 데이터베이스로
        return None

특정 데이터베이스 사용

코드에서 명시적으로 데이터베이스를 지정할 수도 있다:

1
2
3
4
5
6
# 읽기 작업에 특정 데이터베이스 사용
books = Book.objects.using('replica').all()

# 쓰기 작업에 특정 데이터베이스 사용
book.save(using='archive')
Book.objects.using('archive').filter(published_date__year__lt=2000).delete()

ORM 디버깅 및 성능 모니터링

쿼리 확인

Django ORM이 어떤 SQL 쿼리를 생성하는지 확인하는 방법:

1
2
3
# QuerySet의 쿼리 속성 사용
queryset = Book.objects.filter(published_date__year=2023)
print(queryset.query)  # SQL 쿼리 출력

쿼리 로깅

데이터베이스 쿼리를 로깅하여 분석할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        }
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        }
    }
}

Django Debug Toolbar

Django Debug Toolbar는 실행된 쿼리와 실행 시간을 시각적으로 보여주는 도구이다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# settings.py
INSTALLED_APPS = [
    # …
    'debug_toolbar',
]

MIDDLEWARE = [
    # …
    'debug_toolbar.middleware.DebugToolbarMiddleware',
]

INTERNAL_IPS = [
    '127.0.0.1',
]

쿼리 개수 추적

특정 코드 블록에서 실행되는 쿼리 개수를 추적할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from django.db import connection, reset_queries
import time
import functools

def query_debugger(func):
    """쿼리 개수와 시간을 측정하는 데코레이터"""
    @functools.wraps(func)
    def inner_func(*args, **kwargs):
        reset_queries()
        
        start_queries = len(connection.queries)
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        end_queries = len(connection.queries)
        
        print(f"함수: {func.__name__}")
        print(f"쿼리 개수: {end_queries - start_queries}")
        print(f"실행 시간: {(end - start):f}초")
        
        return result
    return inner_func

@query_debugger
def get_books_with_authors():
    # 비효율적인 방법 (N+1 쿼리)
    books = list(Book.objects.all())
    for book in books:
        author = book.author  # 추가 쿼리 발생
    return books

@query_debugger
def get_books_with_authors_optimized():
    # 효율적인 방법 (1개의 쿼리)
    books = list(Book.objects.select_related('author').all())
    for book in books:
        author = book.author  # 추가 쿼리 없음
    return books

ORM 설계 모범 사례 및 성능 최적화

모델 설계 모범 사례

적절한 필드 타입 선택
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 잘못된 예
class Product(models.Model):
    name = models.CharField(max_length=255)  # 너무 긴 max_length
    description = models.CharField(max_length=2000)  # CharField가 아닌 TextField 사용 필요
    price = models.FloatField()  # 금액에는 FloatField보다 DecimalField가 적합
    
# 올바른 예
class Product(models.Model):
    name = models.CharField(max_length=100)  # 적절한 길이
    description = models.TextField()  # 긴 텍스트에는 TextField
    price = models.DecimalField(max_digits=10, decimal_places=2)  # 금액에는 DecimalField
인덱스 활용

자주 검색, 필터링, 정렬되는 필드에는 인덱스를 추가한다:

1
2
3
4
5
6
7
8
9
class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(max_length=13, db_index=True)  # 단일 필드 인덱스
    published_date = models.DateField(db_index=True)  # 자주 검색되는 필드
    
    class Meta:
        indexes = [
            models.Index(fields=['author', 'published_date']),  # 복합 인덱스
        ]
Null과 Blank의 적절한 사용
1
2
3
4
5
# 문자열 필드
description = models.CharField(max_length=200, blank=True, default='')  # null=True 대신 빈 문자열 사용

# 숫자, 날짜 등의 필드
published_date = models.DateField(null=True, blank=True)  # 문자열이 아닌 필드는 null=True 사용

쿼리 최적화 전략

N+1 쿼리 문제 해결
1
2
3
4
5
6
7
8
9
# 비효율적인 코드 (N+1 쿼리)
books = Book.objects.all()
for book in books:
    print(f"{book.title} by {book.author.name}")

# 효율적인 코드 (1개의 쿼리)
books = Book.objects.select_related('author').all()
for book in books:
    print(f"{book.title} by {book.author.name}")
지연 로딩 활용
1
2
3
4
5
6
7
8
# 즉시 평가 (쿼리 즉시 실행)
books_list = list(Book.objects.all())
count = Book.objects.count()

# 지연 평가 (실제로 사용할 때까지 쿼리 실행 안 함)
books = Book.objects.filter(published_date__year=2023)
if some_condition:
    books = books.filter(price__lt=20)  # 추가 필터링
데이터베이스 함수 활용
1
2
3
4
5
6
7
from django.db.models import F, Sum, Count

# 데이터베이스 레벨에서 계산
Book.objects.update(price=F('price') * 1.1)  # 모든 책 가격 10% 인상

# 효율적인 집계
total_sales = Order.objects.aggregate(total=Sum('amount'))

대량 데이터 처리

11.3.1 청크 단위 처리
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.db import transaction

def process_books_in_chunks(chunk_size=1000):
    # 전체 ID 목록 가져오기
    book_ids = Book.objects.values_list('id', flat=True).order_by('id')
    
    # 청크 단위로 처리
    for i in range(0, len(book_ids), chunk_size):
        chunk = book_ids[i:i+chunk_size]
        
        # 트랜잭션으로 청크 단위 처리
        with transaction.atomic():
            books = Book.objects.filter(id__in=chunk)
            for book in books:
                process_book(book)
Iterator 사용
1
2
3
4
5
def process_all_books():
    # 메모리 효율적인 이터레이터 사용
    books = Book.objects.iterator()
    for book in books:
        process_book(book)
비동기 작업 큐 활용

Django와 Celery를 함께 사용하여 대량 작업을 비동기적으로 처리할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# tasks.py (Celery 작업 정의)
@app.task
def process_book(book_id):
    book = Book.objects.get(id=book_id)
    # 책 처리 로직…

# views.py
def process_all_books(request):
    book_ids = Book.objects.values_list('id', flat=True)
    for book_id in book_ids:
        process_book.delay(book_id)  # 비동기 작업 큐에 추가
    return HttpResponse("처리가 백그라운드에서 시작되었습니다.")

용어 정리

용어설명

참고 및 출처