SQLAlchemy의 selectload와 joinload 차이는 무엇일까?
SQLAlchemy를 사용하다 보면 관계된 데이터를 로드하는 방식에 따라 성능이 크게 달라지는 것을 경험하게 됩니다. 특히 selectload
와 joinload
는 자주 사용되는 두 가지 로딩 전략인데, 각각의 특징과 적절한 사용 시점을 알아보겠습니다.
ORM에서의 데이터 로딩 전략
SQLAlchemy에서는 크게 세 가지 로딩 전략을 제공합니다:
- Lazy Loading (기본값)
- Eager Loading (joinedload)
- Select Loading (selectinload)
이 중에서 특히 많이 사용되는 Eager Loading과 Select Loading을 자세히 비교해보겠습니다.
selectload 상세 분석
selectload
는 관계된 데이터를 별도의 SELECT 쿼리로 로드하는 방식입니다.
기본 사용법
from sqlalchemy import select
from sqlalchemy.orm import selectinload
# 기본적인 사용
stmt = select(Post).options(selectinload(Post.comments))
posts = session.execute(stmt).scalars().all()
# 중첩된 관계에서의 사용
stmt = select(Post).options(
selectinload(Post.comments).selectinload(Comment.replies)
)
posts = session.execute(stmt).scalars().all()
생성되는 SQL
-- 게시글 조회
SELECT * FROM posts;
-- 댓글 조회
SELECT * FROM comments
WHERE post_id IN (1, 2, 3, ...);
-- 대댓글 조회 (중첩 관계의 경우)
SELECT * FROM replies
WHERE comment_id IN (1, 2, 3, ...);
성능 특성
- 메모리 효율성: 필요한 데이터만 정확히 가져와 메모리 사용이 효율적
- 쿼리 수: 관계마다 추가 쿼리 발생
- 데이터베이스 부하: 여러 번의 쿼리로 인한 데이터베이스 연결 부하 발생 가능
joinload 상세 분석
joinload
는 JOIN을 사용해 한 번에 모든 데이터를 가져오는 방식입니다.
기본 사용법
from sqlalchemy import select
from sqlalchemy.orm import joinedload
# 기본적인 사용
stmt = select(Post).options(joinedload(Post.comments))
posts = session.execute(stmt).scalars().all()
# 중첩된 관계에서의 사용
stmt = select(Post).options(
joinedload(Post.comments).joinedload(Comment.replies)
)
posts = session.execute(stmt).scalars().all()
생성되는 SQL
-- 단일 관계
SELECT posts.*, comments.*
FROM posts
LEFT OUTER JOIN comments ON posts.id = comments.post_id;
-- 중첩 관계
SELECT posts.*, comments.*, replies.*
FROM posts
LEFT OUTER JOIN comments ON posts.id = comments.post_id
LEFT OUTER JOIN replies ON comments.id = replies.comment_id;
성능 특성
- 메모리 사용: 모든 데이터를 한 번에 로드하여 메모리 사용량 증가
- 쿼리 수: 단일 쿼리로 처리
- 데이터베이스 부하: 복잡한 JOIN으로 인한 데이터베이스 처리 부하 발생 가능
실제 사용 사례와 성능 비교
게시판 시스템 예시
# 게시판 목록 조회 (selectload가 유리)
stmt = select(Board).options(
selectinload(Board.posts),
selectinload(Board.categories)
)
# 게시글 상세 조회 (joinload가 유리)
stmt = select(Post).options(
joinedload(Post.author),
joinedload(Post.comments).joinedload(Comment.author)
).where(Post.id == post_id)
성능 테스트 결과
다양한 데이터 크기에 따른 성능 비교
# 시나리오 1: 10,000개 게시글, 게시글당 평균 5개 댓글
## selectload
- 메모리 사용량: ~100MB
- 실행 시간: ~2초
- 생성된 쿼리 수: 2개
## joinload
- 메모리 사용량: ~400MB
- 실행 시간: ~1.5초
- 생성된 쿼리 수: 1개
# 시나리오 2: 1,000개 게시글, 게시글당 평균 50개 댓글
## selectload
- 메모리 사용량: ~150MB
- 실행 시간: ~3초
- 생성된 쿼리 수: 2개
## joinload
- 메모리 사용량: ~800MB
- 실행 시간: ~4초
- 생성된 쿼리 수: 1개
최적화 팁
1.복합 전략 사용
# 일부는 join으로, 일부는 select로 로드
stmt = select(Post).options(
joinedload(Post.author), # 작은 데이터는join
selectinload(Post.comments) # 큰 데이터는 select
)
2. 조건부 로딩
# 조건에 따라 다른 전략 사용
def get_posts(with_comments=False):
stmt = select(Post)
if with_comments:
stmt = stmt.options(joinedload(Post.comments))
return session.execute(stmt).scalars().all()
주의사항과 고려사항
메모리 관리
joinload
사용 시 메모리 모니터링 필요- 대량 데이터 처리 시 페이지네이션과 함께 사용
쿼리 최적화
- 불필요한 관계 로딩 피하기
- 필요한 컬럼만 선택적으로 로드
데이터베이스 인덱스
- 관련 컬럼에 적절한 인덱스 설정 필요
- JOIN 조건에 사용되는 컬럼 인덱싱
결론
두 전략 모두 각자의 장단점이 있어 상황에 맞게 선택하는 것이 중요합니다. 개인적으로는 기본적으로 selectload
를 사용하고, 성능 최적화가 필요한 특정 상황에서만 joinload
로 전환하는 방식을 선호합니다.
특히 다음 상황에서는 신중한 선택이 필요합니다:
- 대량의 데이터를 다룰 때
- 복잡한 관계를 가진 모델을 다룰 때
- 실시간 성능이 중요한 API를 개발할 때
참고자료
728x90
'개인공부' 카테고리의 다른 글
스프링에서 환경 설정을 어떻게 구분할까? (0) | 2024.11.15 |
---|---|
Gradle의 apply from을 통해 의존성 관리하기 (0) | 2024.11.13 |
[ Django ] 유저 모델을 설계할 때, User, AbstractBaseUser, AbstractUser 중 어떤 클래스를 사용해야 할까? (0) | 2024.11.10 |
[ Pydantic ] Pydantic을 활용한 Serialization(직렬화)와 Deserialization(역직렬화) (0) | 2024.05.30 |
[ Pydantic ] Pydantic의 BaseModel 사용하기 (0) | 2024.05.28 |