Skip to content
somaz edited this page Apr 21, 2025 · 3 revisions

Python ORM 개념 정리


1️⃣ ORM 기초

ORM(Object Relational Mapping)은 객체와 관계형 데이터베이스를 매핑하는 기술이다.

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 데이터베이스 연결
engine = create_engine('sqlite:///example.db', echo=True)  # echo=True로 실행되는 SQL 출력
Base = declarative_base()
Session = sessionmaker(bind=engine)

# 모델 정의
class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    email = Column(String(100), unique=True)
    
    def __repr__(self):
        return f"<User(name='{self.name}', email='{self.email}')>"

# 다양한 데이터베이스 연결 방법
postgres_engine = create_engine('postgresql://username:password@localhost:5432/mydatabase')
mysql_engine = create_engine('mysql+mysqlconnector://username:password@localhost/mydatabase')

# 환경 변수를 사용한 연결 문자열 관리
import os
from dotenv import load_dotenv

load_dotenv()  # .env 파일에서 환경 변수 로드
db_url = os.getenv('DATABASE_URL')
secure_engine = create_engine(db_url)

# 연결 풀링 설정
pooled_engine = create_engine(
    'sqlite:///example.db',
    pool_size=10,                   # 풀에 유지할 연결 수
    max_overflow=20,                # 풀 크기를 초과하여 생성할 수 있는 연결 수
    pool_timeout=30,                # 연결 대기 시간(초)
    pool_recycle=1800               # 연결 재사용 시간(초)
)

특징:

  • 객체-관계 매핑(ORM) 패러다임
  • SQL 코드 작성 없이 데이터베이스 조작
  • 데이터베이스 독립적인 코드 작성 가능
  • 객체 지향적 데이터 접근
  • 자동 스키마 생성 및 관리
  • 다양한 데이터베이스 시스템 지원
  • 연결 풀링 및 세션 관리
  • 타입 안전성 제공


2️⃣ 기본 CRUD 작업

ORM을 사용하여 데이터베이스의 기본 작업인 생성(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 구현한다.

# 테이블 생성
Base.metadata.create_all(engine)

# 데이터 생성 (Create)
def create_user(name, email):
    session = Session()
    try:
        user = User(name=name, email=email)
        session.add(user)
        session.commit()
        return user.id
    except Exception as e:
        session.rollback()
        print(f"사용자 생성 오류: {e}")
        raise
    finally:
        session.close()

# 여러 객체 한 번에 생성
def create_many_users(users_data):
    session = Session()
    try:
        users = [User(name=data['name'], email=data['email']) for data in users_data]
        session.add_all(users)
        session.commit()
        return [user.id for user in users]
    except Exception as e:
        session.rollback()
        print(f"다수 사용자 생성 오류: {e}")
        raise
    finally:
        session.close()

# 데이터 조회 (Read)
def get_user(user_id):
    session = Session()
    try:
        return session.query(User).filter(User.id == user_id).first()
    finally:
        session.close()

# 모든 사용자 조회
def get_all_users():
    session = Session()
    try:
        return session.query(User).all()
    finally:
        session.close()

# 조건부 조회
def find_users_by_email_domain(domain):
    session = Session()
    try:
        return session.query(User).filter(User.email.like(f'%@{domain}')).all()
    finally:
        session.close()

# 페이징 처리
def get_users_paginated(page=1, per_page=10):
    session = Session()
    try:
        return session.query(User).order_by(User.id).offset((page - 1) * per_page).limit(per_page).all()
    finally:
        session.close()

# 데이터 수정 (Update)
def update_user(user_id, name=None, email=None):
    session = Session()
    try:
        user = session.query(User).filter(User.id == user_id).first()
        if user:
            if name:
                user.name = name
            if email:
                user.email = email
            session.commit()
            return True
        return False
    except Exception as e:
        session.rollback()
        print(f"사용자 업데이트 오류: {e}")
        raise
    finally:
        session.close()

# 대량 업데이트
def update_many_users(email_domain, new_domain):
    session = Session()
    try:
        # filter + update를 사용한 벌크 업데이트
        count = session.query(User).filter(
            User.email.like(f'%@{email_domain}')
        ).update(
            {User.email: User.email.op('REPLACE')(f'@{email_domain}', f'@{new_domain}')},
            synchronize_session=False
        )
        session.commit()
        return count
    except Exception as e:
        session.rollback()
        print(f"다수 사용자 업데이트 오류: {e}")
        raise
    finally:
        session.close()

# 데이터 삭제 (Delete)
def delete_user(user_id):
    session = Session()
    try:
        user = session.query(User).filter(User.id == user_id).first()
        if user:
            session.delete(user)
            session.commit()
            return True
        return False
    except Exception as e:
        session.rollback()
        print(f"사용자 삭제 오류: {e}")
        raise
    finally:
        session.close()

# 대량 삭제
def delete_users_by_email_domain(domain):
    session = Session()
    try:
        count = session.query(User).filter(
            User.email.like(f'%@{domain}')
        ).delete(synchronize_session=False)
        session.commit()
        return count
    except Exception as e:
        session.rollback()
        print(f"다수 사용자 삭제 오류: {e}")
        raise
    finally:
        session.close()

# 컨텍스트 매니저를 사용한 세션 관리
from contextlib import contextmanager

@contextmanager
def session_scope():
    """트랜잭션 관리를 위한 세션 컨텍스트 매니저"""
    session = Session()
    try:
        yield session
        session.commit()
    except Exception as e:
        session.rollback()
        raise
    finally:
        session.close()

# 컨텍스트 매니저 사용 예시
def create_user_with_context(name, email):
    with session_scope() as session:
        user = User(name=name, email=email)
        session.add(user)
        return user.id

특징:

  • 완전한 CRUD 연산 지원
  • 자동 트랜잭션 관리
  • 세션 기반 객체 추적
  • 대량 작업 최적화
  • 세션 컨텍스트 관리
  • 조건부 쿼리 빌더
  • 페이징 및 정렬 기능
  • 예외 처리 및 롤백 자동화


3️⃣ 관계 설정

ORM에서 테이블 간의 관계를 객체지향적으로 표현하는 방법이다.

from sqlalchemy import ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime

# 일대다 관계 (One-to-Many)
class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    email = Column(String(100), unique=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # 관계 설정 - 사용자가 삭제되면 게시물도 함께 삭제 (cascade)
    posts = relationship("Post", back_populates="user", cascade="all, delete-orphan")
    comments = relationship("Comment", back_populates="user", cascade="all, delete-orphan")
    
    def __repr__(self):
        return f"<User(name='{self.name}', email='{self.email}')>"

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    content = Column(Text)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # 관계 설정
    user = relationship("User", back_populates="posts")
    comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
    tags = relationship("Tag", secondary="post_tags", back_populates="posts")
    
    def __repr__(self):
        return f"<Post(title='{self.title}')>"

# 다대다 관계 (Many-to-Many)
from sqlalchemy import Table

# 연결 테이블 정의
post_tags = Table('post_tags', Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True)
)

class Tag(Base):
    __tablename__ = 'tags'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    
    # 다대다 관계
    posts = relationship("Post", secondary="post_tags", back_populates="tags")
    
    def __repr__(self):
        return f"<Tag(name='{self.name}')>"

# 추가 관계 - 댓글
class Comment(Base):
    __tablename__ = 'comments'
    
    id = Column(Integer, primary_key=True)
    content = Column(Text, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    post_id = Column(Integer, ForeignKey('posts.id'), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # 관계 설정
    user = relationship("User", back_populates="comments")
    post = relationship("Post", back_populates="comments")
    
    def __repr__(self):
        return f"<Comment(content='{self.content[:20]}...')>"

# 자기참조 관계 (Self-Referential)
class Employee(Base):
    __tablename__ = 'employees'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    manager_id = Column(Integer, ForeignKey('employees.id'), nullable=True)
    
    # 자기참조 관계
    manager = relationship("Employee", remote_side=[id], backref="subordinates")
    
    def __repr__(self):
        return f"<Employee(name='{self.name}')>"

# 관계 데이터 사용 예시
def relation_examples():
    with session_scope() as session:
        # 사용자 생성
        user = User(name="홍길동", email="hong@example.com")
        session.add(user)
        
        # 게시물 생성 및 연결
        post = Post(title="ORM 관계 설정", content="관계 설정 방법을 알아봅시다.", user=user)
        session.add(post)
        
        # 태그 생성 및 연결
        tag1 = Tag(name="ORM")
        tag2 = Tag(name="SQLAlchemy")
        post.tags.extend([tag1, tag2])
        
        # 댓글 생성 및 연결
        comment = Comment(content="좋은 글입니다!", user=user, post=post)
        session.add(comment)
        
        # 직원 관계 설정
        manager = Employee(name="김관리")
        session.add(manager)
        
        employee1 = Employee(name="이사원", manager=manager)
        employee2 = Employee(name="박직원", manager=manager)
        session.add_all([employee1, employee2])

# 관계 쿼리 예시
def query_relations():
    with session_scope() as session:
        # 특정 사용자의 모든 게시물 조회
        user = session.query(User).filter_by(name="홍길동").first()
        if user:
            for post in user.posts:
                print(f"게시물: {post.title}")
                
                # 게시물의 댓글 조회
                for comment in post.comments:
                    print(f"  댓글: {comment.content} (작성자: {comment.user.name})")
                
                # 게시물의 태그 조회
                for tag in post.tags:
                    print(f"  태그: {tag.name}")
        
        # 특정 태그가 있는 모든 게시물 조회
        orm_tag = session.query(Tag).filter_by(name="ORM").first()
        if orm_tag:
            for post in orm_tag.posts:
                print(f"ORM 태그 게시물: {post.title} (작성자: {post.user.name})")
        
        # 관리자와 부하 직원 조회
        manager = session.query(Employee).filter_by(name="김관리").first()
        if manager:
            print(f"관리자: {manager.name}")
            for emp in manager.subordinates:
                print(f"  부하직원: {emp.name}")

특징:

  • 다양한 관계 유형 지원 (일대다, 다대다, 자기참조)
  • 양방향 관계 설정
  • 외래 키 제약 조건
  • 관계 캐스케이드 동작
  • 지연 로딩(Lazy Loading)
  • 즉시 로딩(Eager Loading)
  • 조인 테이블 매핑
  • 객체 그래프 탐색


4️⃣ 고급 쿼리

ORM을 사용한 복잡한 쿼리 작성 및 최적화 방법이다.

from sqlalchemy import and_, or_, not_, func, distinct, text, desc, asc
from sqlalchemy.orm import aliased, contains_eager, joinedload, selectinload

def advanced_query_examples():
    with session_scope() as session:
        # 복합 조건 필터링
        users = session.query(User).filter(
            and_(
                User.name.like('홍%'),
                User.email.contains('@example.com'),
                or_(
                    User.created_at > datetime(2021, 1, 1),
                    not_(User.email.endswith('gmail.com'))
                )
            )
        ).all()
        
        # 정렬
        users = session.query(User).order_by(User.created_at.desc(), User.name).all()
        
        # 집계 함수
        user_count = session.query(func.count(User.id)).scalar()
        post_stats = session.query(
            func.count(Post.id).label('total_posts'),
            func.avg(func.length(Post.content)).label('avg_length')
        ).first()
        
        # 그룹화
        post_counts = session.query(
            User.name,
            func.count(Post.id).label('post_count')
        ).join(Post).group_by(User.id).order_by(desc('post_count')).all()
        
        # 서브쿼리
        from sqlalchemy.sql import exists
        
        active_users = session.query(User).filter(
            exists().where(Post.user_id == User.id)
        ).all()
        
        # 별칭을 사용한 자기 조인
        mgr_alias = aliased(Employee, name='manager')
        results = session.query(
            Employee.name, mgr_alias.name
        ).join(
            mgr_alias, Employee.manager_id == mgr_alias.id
        ).all()
        
        # 윈도우 함수 (SQLAlchemy 1.4+)
        from sqlalchemy.sql import functions as func
        
        stmt = session.query(
            Post,
            func.row_number().over(
                order_by=Post.created_at
            ).label('row_number'),
            func.rank().over(
                partition_by=Post.user_id,
                order_by=Post.created_at.desc()
            ).label('rank')
        ).subquery()
        
        ranked_posts = session.query(stmt).filter(stmt.c.rank == 1).all()
        
        # 원시 SQL 쿼리
        raw_results = session.query(User).from_statement(
            text("SELECT * FROM users WHERE name LIKE :name")
        ).params(name='홍%').all()
        
        return {
            'users': users, 
            'user_count': user_count,
            'post_stats': post_stats,
            'post_counts': post_counts,
            'active_users': active_users,
            'reporting_chain': results,
            'ranked_posts': ranked_posts,
            'raw_results': raw_results
        }

# N+1 문제 해결 - 지연 로딩 vs 즉시 로딩
def n_plus_one_problem():
    # N+1 문제가 발생하는 예제
    with session_scope() as session:
        users = session.query(User).all()  # 1번 쿼리
        
        # 각 사용자별로 추가 쿼리 실행 (N번의 추가 쿼리)
        for user in users:
            print(f"{user.name}의 게시물: {len(user.posts)}")
    
    # 해결책 1: 즉시 로딩(Eager Loading) - joinedload
    with session_scope() as session:
        users = session.query(User).options(joinedload(User.posts)).all()  # JOIN을 통해 1번의 쿼리로 모두 로드
        
        for user in users:
            print(f"{user.name}의 게시물: {len(user.posts)}")  # 추가 쿼리 없음
    
    # 해결책 2: 즉시 로딩 - selectinload (추가 SELECT IN 쿼리 사용)
    with session_scope() as session:
        users = session.query(User).options(selectinload(User.posts)).all()  # 2번의 효율적인 쿼리로 로드
        
        for user in users:
            print(f"{user.name}의 게시물: {len(user.posts)}")  # 추가 쿼리 없음
    
    # 해결책 3: 맞춤 조인 쿼리
    with session_scope() as session:
        users_with_post_count = session.query(
            User, func.count(Post.id).label('post_count')
        ).outerjoin(Post).group_by(User.id).all()
        
        for user, post_count in users_with_post_count:
            print(f"{user.name}의 게시물: {post_count}")

특징:

  • 복잡한 조건 쿼리 구성
  • 고급 조인 기법
  • 집계 및 분석 함수
  • 서브쿼리 및 중첩 쿼리
  • 윈도우 함수 지원
  • 원시 SQL 쿼리 실행
  • 쿼리 최적화 기법
  • N+1 문제 해결 방법


5️⃣ 스키마 관리와 마이그레이션

ORM을 사용한 데이터베이스 스키마 관리 및 변경 추적 방법이다.

# Alembic을 사용한 마이그레이션 (SQLAlchemy와 함께 사용되는 주요 마이그레이션 도구)
"""
설치: pip install alembic
초기화: alembic init migrations
"""

# Alembic 설정 예시 (alembic.ini)
"""
[alembic]
script_location = migrations
sqlalchemy.url = sqlite:///example.db
"""

# 환경 설정 (migrations/env.py)
"""
from alembic import context
from sqlalchemy import engine_from_config, pool

from myapp.models import Base  # 애플리케이션의 모델이 정의된 Base 객체 가져오기

target_metadata = Base.metadata  # 메타데이터 설정

def run_migrations_online():
    connectable = engine_from_config(
        context.config.get_section(context.config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, 
            target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()
"""

# 마이그레이션 생성 및 실행 명령어
"""
마이그레이션 생성: alembic revision --autogenerate -m "Create users table"
마이그레이션 실행: alembic upgrade head
이전 버전으로 롤백: alembic downgrade -1
"""

# 마이그레이션 파일 예시 (migrations/versions/xxxx_create_users_table.py)
"""
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=100), nullable=False),
        sa.Column('email', sa.String(length=100), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email')
    )
    
def downgrade():
    op.drop_table('users')
"""

# 프로그래밍 방식으로 스키마 조작
def schema_manipulation():
    from sqlalchemy import inspect, MetaData
    
    with session_scope() as session:
        # 테이블 메타데이터 검사
        inspector = inspect(engine)
        
        # 테이블 목록 가져오기
        tables = inspector.get_table_names()
        print(f"데이터베이스 테이블: {tables}")
        
        # 테이블 컬럼 정보 가져오기
        for table in tables:
            columns = inspector.get_columns(table)
            print(f"\n{table} 테이블 컬럼:")
            for column in columns:
                print(f"  {column['name']}: {column['type']}")
        
        # 인덱스 정보 가져오기
        for table in tables:
            indexes = inspector.get_indexes(table)
            if indexes:
                print(f"\n{table} 테이블 인덱스:")
                for index in indexes:
                    print(f"  {index['name']} ({', '.join(index['column_names'])})")
        
        # 외래 키 정보 가져오기
        for table in tables:
            fks = inspector.get_foreign_keys(table)
            if fks:
                print(f"\n{table} 테이블 외래 키:")
                for fk in fks:
                    print(f"  {fk['constrained_columns']} -> {fk['referred_table']}.{fk['referred_columns']}")

# 동적 모델 생성
def create_dynamic_model(table_name, columns):
    """동적으로 ORM 모델 클래스 생성"""
    attrs = {
        '__tablename__': table_name,
        '__table_args__': {'extend_existing': True}
    }
    
    # ID 열 추가
    attrs['id'] = Column(Integer, primary_key=True)
    
    # 지정된 열 추가
    for col_name, col_type in columns.items():
        if col_type == 'string':
            attrs[col_name] = Column(String(100))
        elif col_type == 'integer':
            attrs[col_name] = Column(Integer)
        elif col_type == 'datetime':
            attrs[col_name] = Column(DateTime)
        elif col_type == 'boolean':
            attrs[col_name] = Column(Boolean)
    
    # 동적 모델 클래스 생성
    DynamicModel = type(table_name.capitalize(), (Base,), attrs)
    
    # 테이블이 없으면 생성
    if not inspect(engine).has_table(table_name):
        DynamicModel.__table__.create(engine)
    
    return DynamicModel

특징:

  • 자동화된 스키마 생성
  • 버전 관리 마이그레이션
  • 스키마 변경 추적
  • 롤백 기능
  • 메타데이터 검사
  • 테이블 및 컬럼 정보 접근
  • 동적 모델 생성
  • 테이블 변경 자동화


6️⃣ 성능 최적화

ORM 사용 시 성능을 향상시키는 다양한 최적화 기법이다.

# 쿼리 실행 계획 확인
def analyze_query_performance():
    with session_scope() as session:
        # 쿼리 실행 전 출력
        stmt = session.query(User).join(Post).filter(Post.title.like('%ORM%'))
        print(str(stmt))
        
        # 쿼리 실행 시간 측정
        import time
        start = time.time()
        result = stmt.all()
        end = time.time()
        print(f"쿼리 실행 시간: {end - start:.4f}초, 결과 수: {len(result)}")

# 벌크 연산 사용
def bulk_operations():
    with session_scope() as session:
        # 벌크 삽입
        from sqlalchemy.dialects.postgresql import insert as pg_insert
        
        # PostgreSQL의 경우 (UPSERT 기능 지원)
        users_data = [
            {"name": "김철수", "email": "kim@example.com"},
            {"name": "이영희", "email": "lee@example.com"}
        ]
        
        stmt = pg_insert(User.__table__).values(users_data)
        stmt = stmt.on_conflict_do_update(
            index_elements=['email'],
            set_=dict(name=stmt.excluded.name)
        )
        session.execute(stmt)
        
        # 벌크 업데이트 - dialect 독립적 방법
        session.query(User).filter(
            User.email.like('%@example.com')
        ).update(
            {"name": User.name + " (수정됨)"},
            synchronize_session=False
        )
        
        # 벌크 삭제
        deleted = session.query(Post).filter(
            Post.created_at < datetime(2021, 1, 1)
        ).delete(synchronize_session=False)
        
        print(f"{deleted}개의 게시물이 삭제되었습니다.")

# 세션 캐싱 활용
def session_caching_example():
    with session_scope() as session:
        # 첫 번째 쿼리 - 데이터베이스에서 로드
        user1 = session.query(User).filter_by(id=1).first()
        
        # 두 번째 쿼리 - 세션 캐시에서 로드 (추가 쿼리 없음)
        user2 = session.query(User).filter_by(id=1).first()
        
        print(f"동일 객체 여부: {user1 is user2}")  # True
        
        # 세션 캐시 무효화
        session.expire_all()
        
        # 세 번째 쿼리 - 캐시가 무효화되어 다시 데이터베이스에서 로드
        user3 = session.query(User).filter_by(id=1).first()

# 쿼리 최적화 - 필요한 컬럼만 조회
def select_only_needed_columns():
    with session_scope() as session:
        # 모든 컬럼 조회
        users_full = session.query(User).all()
        
        # 필요한 컬럼만 조회
        users_partial = session.query(User.id, User.name).all()
        
        # 개별 엔티티 대신 딕셔너리 형태로 조회
        users_dict = session.query(
            User.id, User.name, User.email
        ).all()

# 커스텀 쿼리 옵티마이저
class QueryOptimizer:
    def __init__(self, session):
        self.session = session
    
    def optimize_query(self, query, limit=None):
        """자동으로 쿼리를 최적화"""
        # 관계 감지 및 자동 조인 로드 추가
        from sqlalchemy.inspection import inspect
        
        # 쿼리 대상 엔티티 확인
        entities = []
        for entity in query.column_descriptions:
            if hasattr(entity['type'], '__tablename__'):
                entities.append(entity['type'])
        
        if not entities:
            return query
        
        # 주 엔티티 선택
        main_entity = entities[0]
        
        # 관계 검사
        mapper = inspect(main_entity)
        relationships = list(mapper.relationships)
        
        # 쿼리에서 사용 중인 관계 감지
        loaded_relationships = []
        for rel in relationships:
            attr_name = rel.key
            if hasattr(main_entity, attr_name) and attr_name in query._joinpoint.entities:
                loaded_relationships.append(attr_name)
        
        # 관계가 로드되면 자동으로 joinedload 추가
        for rel_name in loaded_relationships:
            query = query.options(joinedload(getattr(main_entity, rel_name)))
        
        # 필요한 경우 제한 추가
        if limit is not None:
            query = query.limit(limit)
        
        return query

특징:

  • 쿼리 실행 계획 분석
  • 벌크 연산 활용
  • 세션 캐싱 활용
  • 부분 컬럼 선택적 로드
  • 관계 로딩 최적화
  • 트랜잭션 관리
  • 인덱스 효율적 활용
  • 커스텀 최적화 전략


7️⃣ 실제 애플리케이션 통합

ORM을 실제 애플리케이션에 통합하는 모범 사례와 패턴이다.

# 리포지토리 패턴 구현
class Repository:
    """데이터 액세스 로직을 캡슐화하는 리포지토리 패턴"""
    def __init__(self, model, session_factory):
        self.model = model
        self.session_factory = session_factory
    
    def get(self, id):
        with session_scope() as session:
            return session.query(self.model).filter_by(id=id).first()
    
    def get_all(self):
        with session_scope() as session:
            return session.query(self.model).all()
    
    def find_by(self, **kwargs):
        with session_scope() as session:
            return session.query(self.model).filter_by(**kwargs).all()
    
    def create(self, **kwargs):
        with session_scope() as session:
            instance = self.model(**kwargs)
            session.add(instance)
            session.commit()
            session.refresh(instance)
            return instance
    
    def update(self, id, **kwargs):
        with session_scope() as session:
            instance = session.query(self.model).filter_by(id=id).first()
            if instance:
                for key, value in kwargs.items():
                    setattr(instance, key, value)
                session.commit()
                return instance
            return None
    
    def delete(self, id):
        with session_scope() as session:
            instance = session.query(self.model).filter_by(id=id).first()
            if instance:
                session.delete(instance)
                session.commit()
                return True
            return False

# 애플리케이션 구성 예시 - Flask 웹 애플리케이션
"""
from flask import Flask, request, jsonify
from myapp.models import Base, User
from myapp.repository import Repository
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

app = Flask(__name__)

# 데이터베이스 설정
engine = create_engine('sqlite:///app.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

# 리포지토리 생성
user_repo = Repository(User, Session)

@app.route('/users', methods=['GET'])
def get_users():
    users = user_repo.get_all()
    return jsonify([{
        'id': user.id,
        'name': user.name,
        'email': user.email
    } for user in users])

@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = user_repo.get(user_id)
    if user:
        return jsonify({
            'id': user.id,
            'name': user.name,
            'email': user.email
        })
    return jsonify({'error': 'User not found'}), 404

@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    try:
        user = user_repo.create(**data)
        return jsonify({
            'id': user.id,
            'name': user.name,
            'email': user.email
        }), 201
    except Exception as e:
        return jsonify({'error': str(e)}), 400

@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    data = request.json
    user = user_repo.update(user_id, **data)
    if user:
        return jsonify({
            'id': user.id,
            'name': user.name,
            'email': user.email
        })
    return jsonify({'error': 'User not found'}), 404

@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    success = user_repo.delete(user_id)
    if success:
        return '', 204
    return jsonify({'error': 'User not found'}), 404

if __name__ == '__main__':
    app.run(debug=True)
"""

# 단위 테스트
"""
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from myapp.models import Base, User
from myapp.repository import Repository

class TestUserRepository(unittest.TestCase):
    def setUp(self):
        # 메모리 내 SQLite DB
        self.engine = create_engine('sqlite:///:memory:')
        Base.metadata.create_all(self.engine)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
        self.repo = Repository(User, Session)
    
    def tearDown(self):
        Base.metadata.drop_all(self.engine)
    
    def test_create_user(self):
        user = self.repo.create(name='테스트', email='test@example.com')
        self.assertIsNotNone(user.id)
        self.assertEqual(user.name, '테스트')
    
    def test_get_user(self):
        user = self.repo.create(name='테스트', email='test@example.com')
        fetched_user = self.repo.get(user.id)
        self.assertEqual(fetched_user.name, '테스트')
    
    def test_update_user(self):
        user = self.repo.create(name='테스트', email='test@example.com')
        updated_user = self.repo.update(user.id, name='수정됨')
        self.assertEqual(updated_user.name, '수정됨')
    
    def test_delete_user(self):
        user = self.repo.create(name='테스트', email='test@example.com')
        success = self.repo.delete(user.id)
        self.assertTrue(success)
        self.assertIsNone(self.repo.get(user.id))
"""

특징:

  • 리포지토리 패턴으로 데이터 접근 추상화
  • 웹 애플리케이션 통합 예시
  • 단위 테스트 작성 방법
  • 세션 관리 전략
  • API 엔드포인트 연동
  • 개발-프로덕션 환경 분리
  • ORM 모범 사례 적용
  • 코드 구조화 및 모듈화


주요 팁

모범 사례:

  • 세션 관리 주의
    • 세션 스코프를 명확히 정의하고 항상 닫기
    • 컨텍스트 매니저 패턴 활용
    • 하나의 요청/응답 주기에 하나의 세션 사용
    • 장기 실행 세션 피하기
  • 적절한 인덱스 사용
    • 자주 조회하는 컬럼에 인덱스 추가
    • 복합 인덱스 활용
    • 외래 키 컬럼에 인덱스 적용
    • 불필요한 인덱스 제거
  • Lazy Loading 이해
    • 지연 로딩의 장단점 파악
    • N+1 쿼리 문제 인식
    • 즉시 로딩 적절히 활용
    • 필요에 따라 조인 전략 선택
  • N+1 문제 주의
    • joinedload, selectinload 활용
    • 관계 조회 시 로딩 전략 지정
    • 다수의 개체 조회 시 특히 주의
    • 쿼리 최적화로 불필요한 로딩 방지
  • 벌크 연산 활용
    • 다수 레코드 처리 시 벌크 연산 사용
    • 메모리 사용량 최소화
    • ORM 오버헤드 감소
    • 데이터베이스 부하 분산
  • 캐싱 전략 수립
    • 세션 캐시 활용
    • 외부 캐시(Redis 등) 통합
    • 읽기 빈도가 높은 데이터 캐싱
    • 적절한 캐시 무효화 전략 구현
  • 마이그레이션 관리
    • Alembic 등 전문 도구 활용
    • 버전 관리 시스템과 통합
    • 마이그레이션 테스트 자동화
    • 다운그레이드 경로 유지
  • 트랜잭션 처리
    • 원자적 연산에 트랜잭션 사용
    • 명시적 커밋/롤백 관리
    • 예외 처리와 결합
    • 중첩 트랜잭션 이해


Clone this wiki locally