🗄️ 데이터베이스

ORM

Object-Relational Mapping

객체와 관계형 DB를 매핑하는 기술. SQL 직접 작성 불필요. Prisma, SQLAlchemy, TypeORM.

📖 상세 설명

ORM이란?

ORM(Object-Relational Mapping)은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블을 자동으로 매핑해주는 기술입니다. 개발자가 SQL을 직접 작성하지 않고 프로그래밍 언어의 코드로 데이터베이스를 조작할 수 있게 해줍니다.

ORM의 장점

  • 생산성 향상: 반복적인 CRUD SQL 작성 불필요
  • DB 독립성: 다른 DBMS로 전환이 용이
  • 타입 안정성: 컴파일 타임에 오류 검출
  • 유지보수: 코드 가독성, 객체 지향적 설계
  • 보안: SQL Injection 방어 (자동 이스케이프)

ORM의 단점

  • 성능 오버헤드: 복잡한 쿼리에서 비효율적인 SQL 생성
  • N+1 문제: 연관 데이터 조회 시 쿼리 폭증
  • 학습 비용: ORM 자체의 학습 필요
  • 복잡한 쿼리 한계: 네이티브 SQL이 필요한 경우 존재

주요 ORM 라이브러리

  • Python: SQLAlchemy, Django ORM, Peewee
  • JavaScript/TypeScript: Prisma, TypeORM, Sequelize, Drizzle
  • Java: Hibernate, JPA, MyBatis
  • Ruby: Active Record
  • Go: GORM, Ent

Active Record vs Data Mapper

  • Active Record: 객체가 직접 DB 연산 담당 (Django ORM, Rails Active Record)
  • Data Mapper: 별도의 매퍼가 객체와 DB 분리 (SQLAlchemy, TypeORM)

💻 코드 예제

SQLAlchemy (Python)

Python
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(50), nullable=False)
    email = Column(String(100), unique=True)

    orders = relationship('Order', back_populates='user')

class Order(Base):
    __tablename__ = 'orders'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    total = Column(Integer)

    user = relationship('User', back_populates='orders')

# 세션 생성
engine = create_engine('postgresql://localhost/mydb')
Session = sessionmaker(bind=engine)
session = Session()

# CRUD 연산
# Create
user = User(name='Alice', email='alice@example.com')
session.add(user)
session.commit()

# Read
user = session.query(User).filter_by(email='alice@example.com').first()
users = session.query(User).filter(User.name.like('%Al%')).all()

# Update
user.name = 'Alice Kim'
session.commit()

# Delete
session.delete(user)
session.commit()

Prisma (TypeScript)

Prisma Schema
// schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  orders    Order[]
  createdAt DateTime @default(now())
}

model Order {
  id        Int      @id @default(autoincrement())
  total     Int
  user      User     @relation(fields: [userId], references: [id])
  userId    Int
}
TypeScript
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Create
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    orders: {
      create: [
        { total: 50000 },
        { total: 30000 }
      ]
    }
  },
  include: { orders: true }
})

// Read with relations
const users = await prisma.user.findMany({
  where: {
    email: { contains: 'example.com' }
  },
  include: {
    orders: {
      where: { total: { gte: 10000 } }
    }
  },
  orderBy: { createdAt: 'desc' },
  take: 10
})

// Transaction
await prisma.$transaction(async (tx) => {
  const user = await tx.user.update({
    where: { id: 1 },
    data: { name: 'Alice Kim' }
  })

  await tx.order.create({
    data: { userId: user.id, total: 100000 }
  })
})

N+1 문제와 해결

Python (문제)
# N+1 문제 발생 (1번 유저 쿼리 + N번 주문 쿼리)
users = session.query(User).all()
for user in users:
    print(user.orders)  # 매번 추가 쿼리 발생!
Python (해결)
from sqlalchemy.orm import joinedload, selectinload

# Eager Loading으로 해결
# 1. joinedload: JOIN으로 한 번에 가져오기
users = session.query(User).options(joinedload(User.orders)).all()

# 2. selectinload: IN 절로 별도 쿼리 (추천)
users = session.query(User).options(selectinload(User.orders)).all()

Raw SQL 사용

Python (SQLAlchemy)
from sqlalchemy import text

# ORM으로 어려운 복잡한 쿼리는 Raw SQL 사용
result = session.execute(text("""
    SELECT u.name, COUNT(o.id) as order_count, SUM(o.total) as total_spent
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE o.created_at >= :start_date
    GROUP BY u.id
    HAVING COUNT(o.id) > 5
    ORDER BY total_spent DESC
"""), {'start_date': '2024-01-01'})

for row in result:
    print(row.name, row.order_count, row.total_spent)

💬 현업 대화 예시

주니어 개발자
"ORM 쓰면 SQL 안 배워도 되나요?"
시니어 개발자
"아니, 오히려 SQL 더 잘 알아야 해. ORM이 생성하는 쿼리를 이해해야 성능 문제 해결할 수 있어. 복잡한 쿼리는 어차피 Raw SQL 써야 하고. ORM은 도구일 뿐이야."
백엔드 개발자
"API가 갑자기 느려졌는데 원인을 모르겠어요."
DBA
"로그 보니까 쿼리가 100개 넘게 나가네. 전형적인 N+1 문제야. 연관 데이터 조회하는 부분에 Eager Loading 적용해. joinedload나 selectinload 써봐."
팀장
"새 프로젝트에 ORM 뭐 쓸까요? TypeORM이랑 Prisma 고민 중이에요."
시니어 개발자
"Prisma가 타입 안정성이 훨씬 좋아. 스키마 변경도 마이그레이션 자동 생성되고. 다만 복잡한 쿼리는 제약이 있어서 Raw SQL 병행해야 할 수도 있어. 프로젝트 복잡도에 따라 선택해."

⚠️ 주의사항

⚠️ N+1 문제 항상 주의
연관 데이터 조회 시 N+1 쿼리 발생에 주의하세요. 항상 쿼리 로그를 확인하고, Eager Loading(joinedload, include 등)을 적절히 사용하세요. 성능 문제의 가장 흔한 원인입니다.
⚠️ 생성된 SQL 확인
ORM이 생성하는 SQL을 반드시 확인하세요. 개발 환경에서 쿼리 로깅을 켜두고, 비효율적인 쿼리가 생성되는지 모니터링하세요. EXPLAIN으로 실행 계획도 확인하세요.
⚠️ 트랜잭션 범위
세션/트랜잭션 범위를 명확히 하세요. 트랜잭션이 너무 길면 락 경합 발생, 너무 짧으면 데이터 불일치. 외부 API 호출은 트랜잭션 밖에서 처리하세요.
⚠️ 대량 작업 시 Bulk API 사용
많은 데이터를 처리할 때 ORM 객체를 하나씩 생성하지 마세요. bulk_insert, bulk_update 등 Bulk API를 사용하거나, Raw SQL을 사용해 성능을 확보하세요.

🔗 관련 용어

📚 더 배우기