ORM으로 객체 모델과 관계형 데이터베이스 연결하기
ORM의 객체-테이블 매핑 방식과 SQL 생성 흐름, Lazy Loading·Eager Loading 선택 기준을 정리한다.
2026-08-14 · 최초 발행 2025-10-14
객체와 테이블 사이의 간극을 다루는 ORM
객체지향 코드의 객체 구조와 관계형 데이터베이스의 테이블 구조는 표현 방식이 다르다. ORM(Object-Relational Mapping)은 이 차이에서 생기는 데이터 변환을 자동화해 객체와 테이블을 연결한다. 개발자는 SQL을 매번 직접 작성하지 않고도 관계형 데이터베이스를 다룰 수 있다.
이 매핑은 객체지향 애플리케이션에서 관계형 데이터베이스를 사용할 때의 패러다임 불일치를 줄이는 데 목적이 있다. SQL을 추상화하고, 개발 생산성과 유지보수성을 높이는 기반이 된다.
ORM이 맡는 범위는 다음과 같다.
- CRUD(Create, Read, Update, Delete) SQL 자동 생성
- 클래스와 테이블, 속성과 컬럼의 연결
- 1:1, 1:N, N:M 관계 매핑
- 자바 객체와 DB 레코드 사이의 타입 변환과 로딩 전략 지원
- 선언적 트랜잭션 관리
- 특정 DBMS에 대한 의존성 최소화
메서드 호출이 SQL과 객체 반환으로 이어지는 흐름
ORM은 개발자가 요청한 저장이나 조회 작업을 매핑 정보에 따라 SQL로 바꾼 뒤 실행한다. 데이터베이스의 결과는 다시 애플리케이션이 사용할 객체 형태로 변환된다.
연관 데이터를 언제 읽을지 결정하는 방식
Lazy Loading은 연관 객체가 실제로 참조되는 시점에 데이터를 가져온다. 반대로 Eager Loading은 초기 쿼리 실행 때 연관 데이터까지 함께 로드한다. 어느 쪽이 적합한지는 화면과 조회 패턴에 따라 달라진다.
| 구분 | Lazy Loading | Eager Loading |
|---|---|---|
| 로딩 시점 | 참조 시점에 데이터 로드 | 초기 쿼리 실행 시 모든 데이터 로드 |
| 장점 | 불필요한 쿼리 실행 방지, 메모리 절약 | 즉시 사용 가능, N+1 문제 방지 |
| 단점 | N+1 쿼리 문제 발생 가능 | 초기 로딩 속도 느림, 메모리 사용량 증가 |
| 사용 예 | 상세 화면에서 연관 객체 필요 시 로드 | 목록 화면에서 연관 데이터까지 한 번에 로드 |
| 구현 방식 | 프록시 객체로 지연 처리 | JOIN 또는 fetch 전략으로 즉시 조회 |
지연 로딩 매핑을 코드로 확인하기
다음 의사코드는 User의 posts를 지연 로딩 관계로 선언한 경우다. 사용자를 가져오는 시점에는 게시글을 조회하지 않고, user.posts에 접근할 때 SELECT 쿼리가 실행된다.
class User:
id: int
posts: List[Post] (Lazy)
map(User).to_table("users")
map(User.posts).relation(Post, lazy=True)
# 데이터 조회
user = session.get(User, id=1) # posts 미조회
print(user.posts) # 이 시점에 SELECT 쿼리 실행
SQLAlchemy에서는 관계 속성에 lazy="select"를 지정해 같은 로딩 방식을 설정할 수 있다.
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="select") # Lazy Loading
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", back_populates="posts")
engine = create_engine("sqlite:///example.db")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
u = User(name="Kim")
session.add(u)
session.commit()
조회 성능을 다룰 때는 Lazy Loading을 기본으로 두고, 조회 최적화가 필요한 경우 Eager Loading을 선택할 수 있다. 조인(fetch join)은 N+1 문제를 피하는 방법이며, 2차 캐시는 조회 성능 개선에 활용된다. 복잡한 쿼리는 ORM과 Native SQL을 혼합해 처리할 수 있다.
프레임워크별 SQL 제어와 매핑 방식
| 구분 | Hibernate(JPA) | MyBatis | SQLAlchemy | Django ORM |
|---|---|---|---|---|
| 매핑 방식 | 완전 ORM | 반 ORM(SQL 직접 가능) | 완전 ORM | 완전 ORM |
| SQL 제어 | 자동 + HQL | 직접 SQL 가능 | 자동 + 직접 | 자동 |
| DB 종속성 | 낮음 | 중간 | 낮음 | 낮음 |
| 학습 곡선 | 높음 | 중간 | 중간 | 낮음 |
| 주요 특징 | 강력한 캐싱, 풍부한 매핑 옵션 | SQL 친화적 | Pythonic, 유연성 | Django와 통합 강점 |
ORM은 객체지향 개발과 관계형 데이터베이스를 연결하지만, 생성되는 SQL을 보지 않아도 되는 기술은 아니다. 로딩 전략과 SQL 최적화를 함께 검토하고, ORM이 만든 쿼리를 분석·튜닝하는 작업이 필요하다.