백엔드

Django ORM, 이것만 알면 된다! 개념·종류·실습 올인원 가이드

지식소 채움이 2025. 8. 21. 08:42

이 글은 “ORM이 뭐고 왜 쓰는가?”부터 시작해, Django ORM을 실무에서 제대로 쓰기 위한 핵심 기능과 종류(패턴/상속/쿼리/관계)를 한 번에 정리한 블로그 포스트입니다. 실습 가능한 코드와 체크리스트를 곁들였습니다.


1) ORM이란 무엇인가?

ORM(Object–Relational Mapping)객체(클래스/인스턴스)와 관계형 데이터베이스(테이블/행) 사이를 자동으로 매핑해 주는 기술입니다.

  • 파이썬 코드로 모델을 다루면 ORM이 내부에서 SQL을 생성·실행하고 결과를 다시 객체로 돌려줍니다.
  • 장점: 생산성↑, 가독성↑, SQL 인젝션 위험↓(파라미터 바인딩), DB 의존도↓
  • 단점: SQL이 안 보이기 때문에 성능 병목(N+1, 불필요한 칼럼/조인) 이 숨어들기 쉽습니다.

ORM 패턴의 “종류”

  • Active Record: 모델 인스턴스가 save(), delete()를 직접 수행(테이블=클래스, 행=객체).
    Django ORM은 일반적으로 Active Record 계열로 분류됩니다.
  • Data Mapper: 도메인 모델과 DB 접근이 분리(매퍼/리포지토리). 예: SQLAlchemy(Core/ORM).

로딩 전략의 “종류”

  • Lazy Loading(지연 로딩): 실제 접근 시 쿼리 실행(기본).
  • Eager Loading(사전 적재): 관계 데이터를 미리 합쳐서 가져오기(select_related, prefetch_related).

2) Django ORM 핵심 구성요소

  • Model: 테이블 스키마 + 도메인 로직
  • Field: 칼럼의 타입/옵션 (예: CharField, IntegerField, DateTimeField, JSONField …)
  • Manager: objects처럼 쿼리 시작점 제공
  • QuerySet: 필터·정렬·슬라이싱을 체인으로 연결하는 지연 평가 가능한 쿼리 객체
  • Migration: 모델 변경을 DB 스키마로 반영하는 버전 관리(DDL)
  • Database Backend: ENGINE(PostgreSQL, MySQL, SQLite 등)

3) 빠른 시작(10분 실습)

# settings.py
DATABASES = {
  "default": {
    "ENGINE": "django.db.backends.postgresql",  # 혹은 sqlite3/mysql
    "NAME": "appdb", "USER": "app", "PASSWORD": "secret",
    "HOST": "127.0.0.1", "PORT": "5432",
  }
}
# app/models.py
from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=100, unique=True)

class Author(models.Model):
    name = models.CharField(max_length=100, db_index=True)

class Book(models.Model):
    title = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    published_at = models.DateField(null=True, blank=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT, related_name="books")
    tags = models.ManyToManyField("Tag", blank=True, related_name="books")

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
python manage.py makemigrations
python manage.py migrate
python manage.py shell
from app.models import *
a = Author.objects.create(name="홍길동")
p = Publisher.objects.create(name="파이콘프레스")
b = Book.objects.create(title="장고 첫걸음", price=25000, author=a, publisher=p)
b.tags.add(Tag.objects.create(name="Django"), Tag.objects.create(name="Python"))

4) CRUD와 대표 API 모음

Create

Book.objects.create(title="두 번째 책", price=18000, author=a, publisher=p)
Book.objects.bulk_create([
    Book(title=f"시리즈 {i}", price=15000, author=a, publisher=p) for i in range(100)
])

Read

# 단건
Book.objects.get(id=1)             # 없거나 2건 이상이면 예외
Book.objects.filter(title__icontains="장고").first()  # 안전한 단건 패턴

# 조건/룩업
qs = Book.objects.filter(
    price__gte=10000,
    author__name__startswith="홍",
    published_at__range=("2024-01-01", "2024-12-31")
).exclude(publisher__name="테스트출판")

# 정렬/제한
qs = qs.order_by("-published_at").only("id","title","author__name")[:20]

# 사전 적재(Eager Loading)
qs = qs.select_related("author","publisher").prefetch_related("tags")

Update

# 단건 수정
b = Book.objects.get(id=1)
b.price = 27000
b.save(update_fields=["price"])

# 대량 수정
Book.objects.filter(author=a).update(price=18000)

# F-expression (동시성 안전한 누적)
from django.db.models import F
Book.objects.update(price=F("price") + 1000)

# upsert
Book.objects.update_or_create(
    title="장고 첫걸음", defaults={"price": 26000, "publisher": p}
)

Delete

Book.objects.filter(price__lt=10000).delete()  # 쿼리셋 일괄 삭제
b.delete()

5) 쿼리 작성의 “종류” (필수 치트시트)

  • 기본: filter(), exclude(), get(), order_by(), distinct(), 슬라이싱
  • 집계/그룹: aggregate(), annotate() + Count, Sum, Avg, Min, Max
  • 조건식: Case, When, Coalesce, Greatest/Least
  • 서브쿼리: Subquery, OuterRef, Exists
  • 수학/문자열/날짜 함수: Func, Lower, Upper, Length, Concat, TruncDate …
  • 윈도 함수: Window + RowNumber, Rank 등 (PostgreSQL 권장)
  • Q 객체(복합 조건):
  • from django.db.models import Q Book.objects.filter(Q(title__icontains="장고") | Q(tags__name="Python"))
  • 성능 튜닝: only(), defer(), select_related(), prefetch_related(), iterator(), select_for_update()

6) 관계의 “종류”와 올바른 사용

6.1 ForeignKey / OneToOne

  • select_related("fk_field")로 조인 1번에 가져오기(Active Record 특유의 N+1 방지)
  • on_delete 옵션: CASCADE, PROTECT, SET_NULL, SET_DEFAULT, DO_NOTHING, SET(callable)
Book.objects.select_related("author","publisher")[:50]

6.2 ManyToMany

  • 조인으로 한 번에 싣기 어렵기 때문에 prefetch_related("m2m") 권장
  • 추가 속성이 필요하면 through 모델을 정의
class BookTag(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    weight = models.PositiveIntegerField(default=1)

class Book(models.Model):
    ...
    tags = models.ManyToManyField(Tag, through="BookTag")
books = Book.objects.prefetch_related("tags")
for b in books:
    tag_names = [t.name for t in b.tags.all()]  # 추가 쿼리 없음

7) 모델 “상속”의 종류

  • Abstract Base Class: 공통 필드/메서드만 물려주고 테이블은 없음.
  • class TimeStamped(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class Meta: abstract = True class Book(TimeStamped):...
  • Multi-table Inheritance: 부모/자식 각각 테이블 생성(조인 발생).
  • Proxy Model: 스키마 변경 없이 행동만 바꾸는 가벼운 상속.

8) 제약조건·인덱스(정확성과 성능)

from django.db import models
from django.db.models import Q

class Book(models.Model):
    ...
    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["title", "publisher"], name="uniq_title_per_publisher"),
            models.CheckConstraint(check=Q(price__gte=0), name="price_non_negative"),
        ]
        indexes = [
            models.Index(fields=["-published_at", "author"]),
        ]
  • UniqueConstraint(권장)로 복합 유니크,
  • **CheckConstraint**로 도메인 룰,
  • Index로 빈번한 필터/정렬 가속.
  • 과도한 인덱스는 쓰기 성능을 떨어뜨리므로 사용 패턴을 관찰해 고르세요.

9) 트랜잭션과 동시성

from django.db import transaction

with transaction.atomic():
    b = Book.objects.select_for_update().get(id=1)  # 행 잠금
    b.price += 1000
    b.save()
  • transaction.atomic()으로 한 단위로 커밋/롤백
  • select_for_update()로 동일 행의 경쟁 갱신 충돌 방지
  • F-expression으로 누적/증감을 DB에서 원자적으로 처리

10) 커스텀 매니저/쿼리 셋

from django.db import models

class BookQuerySet(models.QuerySet):
    def published(self):
        return self.filter(published_at__isnull=False)

class BookManager(models.Manager):
    def get_queryset(self):
        return BookQuerySet(self.model, using=self._db).select_related("author","publisher")
    def cheap(self):
        return self.get_queryset().filter(price__lt=10000)

class Book(models.Model):
    ...
    objects = BookManager()
  • 재사용 가능한 필터/사전 적재를 캡슐화해 중복 제거 + 성능 표준화.

11) 유효성 검사와 데이터 정합성

  • 모델 단: clean()/full_clean() 오버라이드(폼/시리얼라이저와 함께 쓰면 강력).
  • DB 단: constraints, NOT NULL, unique로 최종 방어선 구축.
  • 삭제 규칙: 외래키 on_delete로 도메인 불변식을 보장.

12) 마이그레이션(스키마 버전 관리)

  • makemigrations → 변경 감지, 파일 생성
  • migrate → 실제 DB 변경
  • 데이터 마이그레이션: RunPython으로 데이터 변환/이관
  • 운영에서는 락/다운타임 고려: 대용량 테이블은 칼럼 추가 후 점진 채움, 인덱스 동시 생성 옵션 등 전략 수립

13) 성능 베스트 프랙티스 요약

  1. 목록/상세에서 N+1 제거: FK/O2O는 select_related, M2M/역참조는 prefetch_related.
  2. 필드 다이어트: only()/defer()로 필요한 칼럼만.
  3. 집계/존재 여부: count()/exists() 활용, len(qs)로 세지 않기.
  4. 대량 작업: bulk_create, bulk_update, iterator()(메모리 절약), 배치 크기 조정.
  5. 쿼리 수 확인: 개발 중 django-debug-toolbar, connection.queries, QuerySet.explain().
  6. 인덱스: 사용 패턴 기반으로 추가/튜닝, 불필요한 인덱스는 제거.
  7. 비동기 뷰: ORM은 기본 동기이므로 sync_to_async로 감싸 사용(블로킹 주의).

14) 멀티 DB, 라우팅, 리드 레플리카

# settings.py
DATABASES = {
  "default": {...},           # write
  "replica": {...},           # read
}
# 쓰기/읽기 분리 사용 예
Book.objects.using("replica").filter(...)     # 읽기
Book.objects.using("default").create(...)     # 쓰기
# 간단 라우터 예시
class ReadWriteRouter:
    def db_for_read(self, model, **hints): return "replica"
    def db_for_write(self, model, **hints): return "default"
DATABASE_ROUTERS = ["path.to.ReadWriteRouter"]

15) Raw SQL이 필요한 순간

ORM으로 표현이 어렵거나 DB 고유 기능을 써야 할 때:

for row in Book.objects.raw("SELECT id, title FROM app_book WHERE price > %s", [20000]):
    print(row.id, row.title)

또는 커서:

from django.db import connection
with connection.cursor() as cur:
    cur.execute("SELECT COUNT(*) FROM app_book WHERE price > %s", [20000])
    count, = cur.fetchone()

원칙: ORM 우선, Raw SQL 보완. Raw를 쓰면 테스트/이식성/보안(파라미터 바인딩 유지)도 같이 챙기기.


마무리

ORM은 “코드는 단순하게, 쿼리는 정확하게”라는 목표를 돕는 도구입니다. Django ORM은 배우기 쉬운 Active Record 스타일과 강력한 QuerySet API 덕분에 빠른 개발충분한 성능을 동시에 노릴 수 있습니다.
오늘 프로젝트에서 가장 느린 목록 화면 하나를 골라, 이 글의 체크리스트대로 select_related/prefetch_related와 only()를 적용해 보세요. 쿼리 수가 줄고 응답 시간이 눈에 띄게 개선될 것입니다.