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로 바꾼 뒤 실행한다. 데이터베이스의 결과는 다시 애플리케이션이 사용할 객체 형태로 변환된다.

데이터베이스ORM 프레임워크개발자데이터베이스ORM 프레임워크개발자객체 생성/저장 요청 (session.add, save 등)매핑 정보 확인 (클래스-테이블, 속성-컬럼)SQL 생성 (INSERT, SELECT 등)SQL 실행실행 결과 반환(ResultSet)매핑된 객체 반환

연관 데이터를 언제 읽을지 결정하는 방식

Lazy Loading은 연관 객체가 실제로 참조되는 시점에 데이터를 가져온다. 반대로 Eager Loading은 초기 쿼리 실행 때 연관 데이터까지 함께 로드한다. 어느 쪽이 적합한지는 화면과 조회 패턴에 따라 달라진다.

구분 Lazy Loading Eager Loading
로딩 시점 참조 시점에 데이터 로드 초기 쿼리 실행 시 모든 데이터 로드
장점 불필요한 쿼리 실행 방지, 메모리 절약 즉시 사용 가능, N+1 문제 방지
단점 N+1 쿼리 문제 발생 가능 초기 로딩 속도 느림, 메모리 사용량 증가
사용 예 상세 화면에서 연관 객체 필요 시 로드 목록 화면에서 연관 데이터까지 한 번에 로드
구현 방식 프록시 객체로 지연 처리 JOIN 또는 fetch 전략으로 즉시 조회

지연 로딩 매핑을 코드로 확인하기

다음 의사코드는 Userposts를 지연 로딩 관계로 선언한 경우다. 사용자를 가져오는 시점에는 게시글을 조회하지 않고, 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이 만든 쿼리를 분석·튜닝하는 작업이 필요하다.

ORM객체지향관계형 데이터베이스SQLAlchemyJPA