이 글은 “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) 성능 베스트 프랙티스 요약
- 목록/상세에서 N+1 제거: FK/O2O는 select_related, M2M/역참조는 prefetch_related.
- 필드 다이어트: only()/defer() 로 필요한 컬럼만.
- 집계/존재 여부: count()/exists() 활용, len(qs)로 세지 않기.
- 대량 작업: bulk_create, bulk_update, iterator()(메모리 절약), 배치 크기 조정.
- 쿼리 수 확인: 개발 중 django-debug-toolbar, connection.queries, QuerySet.explain().
- 인덱스: 사용 패턴 기반으로 추가/튜닝, 불필요한 인덱스는 제거.
- 비동기 뷰: 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()를 적용해 보세요. 쿼리 수가 줄고 응답 시간이 눈에 띄게 개선될 것입니다.
'백엔드' 카테고리의 다른 글
Docker 멀티 컨테이너와 Compose, CI/CD 배포 (0) | 2025.09.02 |
---|---|
Django REST Framework 기초와 JWT 인증 이해하기 (1) | 2025.08.20 |
Django ORM 기초: 개념과 예제 (2) | 2025.06.20 |
Docker의 실무 활용 사례 및 사용법 (0) | 2025.06.09 |
Django 프로젝트의 settings.py 주요 설정과 활용법 (3) | 2025.06.04 |