반응형
SQLAlchemy ORM의 로딩 전략 옵션에 대해 알아보자.
이 개념은 단순한 옵션 차이를 넘어, 쿼리 성능, 데이터 처리 효율, N+1 문제 발생 여부에 직결된다.
- Lazy Loading, Eager Loading 이란?
- 실무 예제
- 두 방식의 차이
- 언제 어떤 방식으로 써야하는가?
Lazy Loading이란? (필요할 때 실행)
Lazy Loading은 관계가 설정된 객체를 접근할 때마다 별도로 SELECT 쿼리를 실행하는 방식입니다.
예제 모델
class Author(Base):
__tablename__ = 'author'
id = Column(Integer, primary_key=True)
books = relationship("Book", backref="author")
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("author.id"))
Lazy Loading 예시
authors = session.query(Author).all()
for author in authors:
for book in author.books:
print(book.title)
이 경우 SQLAlchemy는:
- 첫 번째 쿼리로 모든 Author를 가져오고
- 각 author.books 접근 시마다 개별적으로 Book 쿼리를 실행합니다
문제: Author가 100명이라면, Book 쿼리는 100번 발생 → N+1 문제 발생
Eager Loading이란? (함께 조회)
Eager Loading은 관계 데이터를 사전에 함께 쿼리하여 불필요한 추가 쿼리를 막는 방식입니다.
SQLAlchemy는 대표적으로 두 가지 eager loading 방식을 지원합니다:
1. joinedload(): JOIN으로 한 번에 가져오기
from sqlalchemy.orm import joinedload
authors = session.query(Author).options(joinedload(Author.books)).all()
- Author와 Book을 JOIN해서 한 번의 쿼리로 가져옴
- SQL 예시:
- SELECT * FROM author JOIN book ON author.id = book.author_id
2. selectinload(): 서브쿼리로 다 가져오기
from sqlalchemy.orm import selectinload
authors = session.query(Author).options(selectinload(Author.books)).all()
- Author 먼저 가져오고,
- Book은 WHERE author_id IN (...) 방식으로 가져옴 (두 번째 쿼리)
- SQL 예시:
- SELECT * FROM book WHERE author_id IN (1, 2, 3, ...);
Lazy vs Eager: 쿼리 비교
방식 | 쿼리 수 | join 사용 | 중복 가능성 | 추천 상황 |
Lazy (default) | 1 + N | ❌ | 없음 | 소규모 관계일 때 |
joinedload() | 1 | ✅ | 관계 수 많을 때 중복 로우 증가 | 1:N 관계가 적을 때 |
selectinload() | 2 | ❌ | 없음 | 대량 데이터 관계 처리 시 |
Eager Loading의 유의점
joinedload()
장점:
- 한 번의 JOIN 쿼리로 전체 로딩
- 루프 중 관계 접근 시 쿼리 추가 없음
주의점:
- 카디널리티 폭발:
- 관계가 1:N이면 부모 레코드가 N번 중복됨 (메모리 부하 증가)
- pagination 시 LIMIT/OFFSET이 의도와 다르게 작동할 수 있음
- JOIN 대상이 많을 경우 성능 저하
- 여러 관계를 동시에 joinedload 시 복잡한 쿼리로 인해 느려질 수 있음
예시:
- Author 100명, 각 Author가 Book 5권 보유
- joinedload() 시 실제 SQL 결과는 500 rows 생성
- ORM은 중복 제거를 하더라도, 네트워크, 메모리, 쿼리 파싱 비용 증가는 피할 수 없음
selectinload()
장점:
- 관계 수 많을 때 더 안정적 (JOIN 없이 가져옴)
- SQL이 간결하고 메모리 낭비 없음
주의점:
- IN 절이 너무 길어질 수 있음 (수천 개의 ID 포함 시 일부 DBMS에서 문제)
- 깊은 관계에서 중첩 selectinload() 사용 시 쿼리 수 증가
- 성능이 떨어질 수 있음 (예: A → B → C 연쇄 관계)
언제 어떤 방식 써야 할까?
상황 | 추천 방식 | 이유 |
소규모 관계 또는 단일 조회 | Lazy (기본값) | 간단하고 빠름 |
리스트 루프 안에서 관계 접근 | selectinload() | N+1 제거, 메모리 안정적 |
적은 수의 관계를 즉시 다 조회해야 할 때 | joinedload() | 쿼리 1회로 처리 가능 |
pagination 사용 또는 중복 방지 필요 | selectinload() | 중복 없는 구조 유지 |
마무리
lazy loading으로 인한 N+1 문제는, 관계 데이터를 순회하는 루프 안에서 반드시 확인해야 할 중요한 포인트입니다.
반응형
'Python' 카테고리의 다른 글
[Python] 예외 체이닝: 더 나은 디버깅을 위한 예외 처리 전략 (0) | 2025.05.09 |
---|---|
[SQLAlchemy] 같은 테이블을 참조하는 방법 (Self-Referential Join) (0) | 2025.05.09 |
[SQLAlchemy] 윈도우 함수 orm으로 사용 (0) | 2025.05.09 |
[Python] Python으로 Slack 메시지 보내기 (채널 & DM) (0) | 2025.04.22 |
[Python] .style.yapf YAPF (feat. Python formatter) (0) | 2025.04.16 |