SQLAlchemy

Mapped와 DynamicMapped의 정리 - 1:1, 1:n, n:m & uselist 포함

bluebamus 2025. 2. 17.

1. Mapped[T]

   - SQLAlchemy 2.0에서는 기존의 Column()과 relationship()을 사용할 때 더 명확한 타입 힌트를 제공하기 위해 Mapped[T]가 도입되었다.

 

   1) Mapped[T]의 역할

      - 타입 안정성을 제공하여 코드 가독성을 높임
      - Column()뿐만 아니라 relationship()에도 사용 가능
      - Optional[T], List[T] 등과 조합하여 컬럼과 관계 속성의 타입을 명확하게 표현

 

   2) Mapped[T] 사용 방법

from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from sqlalchemy.ext.declarative import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False)
    age: Mapped[Optional[int]] = mapped_column(nullable=True)  # NULL 허용

    posts: Mapped[list["Post"]] = relationship(back_populates="author")

class Post(Base):
    __tablename__ = "post"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(nullable=False)
    content: Mapped[str] = mapped_column(nullable=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))

    author: Mapped["User"] = relationship(back_populates="posts")

 

   3) Mapped[T]의 장점

      1. 타입 힌트를 명확하게 제공

         - Mapped[int], Mapped[str]처럼 명확한 데이터 타입 지정 가능

         - Mapped[Optional[T]] 사용하여 NULL 허용 여부 표현

 

      2. 리스트 타입을 명확하게 표현

         - Mapped[list["Post"]]을 사용하여 1:N 관계를 더 직관적으로 정의 가능

 

      3. 런타임 성능 최적화

         - 기존 SQLAlchemy 1.x에서 사용되던 Column()과 relationship() 방식보다 런타임에서의 속성 처리 성능이 향상됨

 

2. DynamicMapped[T]

   - DynamicMapped[T]는 SQLAlchemy ORM에서 lazy-loading 방식으로 관계 속성을 다룰 때 사용된다.

   - SQLAlchemy에서 기본적으로 relationship()을 사용할 때, 데이터를 list로 로드하는데, DynamicMapped를 사용하면 데이터를 즉시 가져오지 않고 쿼리 객체로 유지할 수 있다.

 

   1) DynamicMapped[T]의 역할

      - relationship()을 동적으로 처리하는 방식
      - dynamic 로딩 옵션을 사용할 때 Mapped 대신 DynamicMapped를 사용
      - 관계된 데이터를 즉시 로드하는 것이 아니라 필요할 때 쿼리를 실행하도록 만듦

 

   2) DynamicMapped[T] 사용 방법

from sqlalchemy.orm import DynamicMapped, Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False)

    # DynamicMapped를 사용하여 posts 관계를 동적으로 로드
    posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")

class Post(Base):
    __tablename__ = "post"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(nullable=False)
    content: Mapped[str] = mapped_column(nullable=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))

    author: Mapped["User"] = relationship(back_populates="posts")

 

   3) DynamicMapped[T]의 특징

      1. lazy="dynamic"과 함께 사용됨

         - DynamicMapped는 관계를 동적 쿼리 객체(Query)로 반환
         - 즉, user.posts를 호출해도 즉시 데이터를 가져오지 않고 필요할 때만 쿼리를 실행

 

      2. 필터링이 가능함

user = session.get(User, 1)
filtered_posts = user.posts.filter(Post.title == "SQLAlchemy Guide").all()

 

         - user.posts는 리스트가 아니라 Query 객체이므로 필터링 및 추가적인 데이터베이스 질의가 가능

         - 만약 Mapped[list["Post"]]을 사용했다면, user.posts를 호출하는 즉시 모든 데이터가 로드됨

 

      3. 대량 데이터 처리에 유용

         - DynamicMapped를 사용하면 관계된 데이터가 많을 때도 메모리를 낭비하지 않고 필요할 때만 데이터를 로드
         - 예를 들어, 수천 개의 Post가 존재하는 User를 불러올 때 Mapped[list["Post"]]을 사용하면 모든 데이터를 한 번에 가져와서 성능 문제가 발생할 수 있음

         - DynamicMapped는 이를 해결하기 위해 쿼리를 동적으로 실행하여 필요한 데이터만 가져옴

 

      4. DynamicMapped 사용 예시

         - 일반적인 relationship()의 문제점

from sqlalchemy.orm import relationship, Mapped
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = 'user'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")

 

         - 위 코드에서는 posts가 즉시 로딩(eager loading)되므로, User 객체를 가져올 때 관련된 모든 Post가 함께 로드된다.
         - 사용하지 않을 데이터도 미리 가져오므로 불필요한 성능 저하가 발생할 수 있다.

from sqlalchemy.orm import DynamicMapped

class User(Base):
    __tablename__ = 'user'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")

 

         - posts가 즉시 리스트로 변환되지 않고, 실행 가능한 SQLAlchemy Query로 유지됨
         - 데이터가 필요할 때만 query를 실행하여 가져올 수 있음

 

         - 데이터 접근 방식 비교

# 기존 relationship()
user = session.get(User, 1)
print(user.posts)  # 리스트 반환 (즉시 로딩됨)

# DynamicMapped 사용
user = session.get(User, 1)
print(user.posts)  # Query 객체 반환
print(user.posts.filter(Post.title == "Hello").all())  # 필요한 데이터만 로드

 

      5. DynamicMapped가 데이터를 실제로 가져오는 시점

         - DynamicMapped 속성은 Query 객체를 반환하기 때문에, all(), first(), one(), one_or_none(), count(), exists() 등을 호출해야 실제 데이터를 가져온다.

user = session.get(User, 1)

# posts는 Query 객체 상태
print(user.posts)  # <sqlalchemy.orm.dynamic.AppenderQuery object at ...>

# 여기서 실제 SQL 실행 (데이터를 로드하는 시점)
posts = user.posts.all()
print(posts)  # 리스트 반환됨

 

         - 이터레이션을 실행할 때 (for 루프)

             - DynamicMapped 속성은 Query 객체이므로, for 루프에서 순회할 때도 실제 SQL이 실행된다.

for post in user.posts:
    print(post.title)  # 여기서 실제 SQL 실행

 

         - 정리

상황 쿼리 실행 여부
user.posts 접근 ❌ (Query 객체 반환)
user.posts.all() ✅ (쿼리 실행)
for post in user.posts: ✅ (쿼리 실행)
user.posts.filter(...).all() ✅ (쿼리 실행)
user.posts.count() ✅ (COUNT(*) 실행)
session.query(user.posts.exists()).scalar() ✅ (EXISTS 실행)

 

3. Mapped[T] vs DynamicMapped[T] 비교

  Mapped[T] DynamicMapped[T]
주 용도 일반적인 컬럼 및 관계 속성 정의 관계 속성을 동적 로딩할 때 사용
데이터 로딩 방식 즉시 로딩 (lazy="select" 기본값) 쿼리를 실행해야 데이터 로드 (lazy="dynamic")
리스트 형태 지원 Mapped[list[T]] 사용 가능 DynamicMapped[T]는 Query 객체를 반환
필터링 가능 여부 user.posts를 호출하면 즉시 리스트 반환 user.posts.filter(...).all()처럼 쿼리 필터링 가능
대량 데이터 처리 대량 데이터를 로드할 경우 성능 문제 발생 가능 필요한 데이터만 로드하여 성능 최적화

 

4. 정리

   - Mapped[T]는 일반적인 컬럼 및 관계 속성을 정의할 때 사용

   - DynamicMapped[T]는 lazy-loading이 필요할 때 사용하며, Query 객체를 반환하여 필터링 가능

   - 대량의 관계 데이터를 처리할 때는 DynamicMapped를 사용하여 메모리 사용량을 줄이고 필요할 때만 쿼리를 실행하는 것이 유리

 

   1) 언제 Mapped vs DynamicMapped를 사용할까?

      - 대부분의 경우 Mapped[list[T]]을 사용하면 충분

      - DynamicMapped는 대량 데이터가 포함된 1:N, N:M 관계에서 사용하면 성능을 최적화할 수 있음

         - 예: User가 수천 개의 Post를 가질 경우 Mapped[list[Post]]는 한 번에 모든 데이터를 로딩하므로 비효율적

         - DynamicMapped["Post"]를 사용하면 user.posts.filter(...)처럼 필요한 데이터만 로드 가능

 

5. 1:1 관계 (One-to-One), 1:N 관계 (One-to-Many), N:M 관계 (Many-to-Many) + 중간 테이블 설정

   1) 1:1 관계 (One-to-One)

      1. 특징

         - 한 개의 엔티티가 오직 하나의 엔티티와 연결됨
         - relationship()에서 uselist=False 설정
         - Foreign Key를 Unique로 설정해야 함

 

      2. 설정 예시

from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey, UniqueConstraint

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    profile: Mapped["UserProfile"] = relationship("UserProfile", back_populates="user", uselist=False)

class UserProfile(Base):
    __tablename__ = "user_profile"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True)  # 1:1 관계를 위해 unique=True 설정
    bio: Mapped[str] = mapped_column(nullable=True)

    user: Mapped["User"] = relationship("User", back_populates="profile")

 

      3. 1:1 관계 설정 핵심

         - ForeignKey("user.id")에 unique=True를 설정하여 한 개의 UserProfile만 특정 User와 연결되도록 보장
         - relationship()에서 uselist=False를 설정하여 1:1 관계를 명확하게 표현
         - back_populates를 사용하여 양방향 참조 가능

 

   2) 1:N 관계 (One-to-Many)

      1. 특징

         - 한 개의 엔티티가 여러 개의 엔티티를 가질 수 있음
         - 부모 테이블은 1개, 자식 테이블은 여러 개의 참조 가능
         - Mapped[list[T]] 또는 DynamicMapped[T] 사용 가능

 

      2. 설정 예시

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "post"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(nullable=False)
    content: Mapped[str] = mapped_column(nullable=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))

    author: Mapped["User"] = relationship("User", back_populates="posts")

 

      3. 1:N 관계 설정 핵심

         - User.posts에 Mapped[list["Post"]]를 사용하여 여러 개의 Post를 포함할 수 있도록 설정
         - Post 테이블의 author_id가 User의 id를 ForeignKey로 참조
         - relationship()을 통해 back_populates="author"로 양방향 매핑 가능

 

      4. DynamicMapped를 사용한 1:N 관계

         - 만약 User가 수천 개의 Post를 가질 경우, Mapped[list["Post"]]를 사용하면 성능 문제가 발생할 수 있다.
         - 이를 방지하려면 DynamicMapped["Post"]를 사용하여 데이터를 필요할 때만 로드하도록 설정할 수 있다.

         - user.posts.all() 또는 user.posts.filter(...).all()을 실행해야 데이터가 로드된다.

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")

 

   3) N:M 관계 (Many-to-Many)

      1. 특징

         - 다대다 관계에서는 중간 테이블 (association table) 이 필요
         - relationship()에서 secondary 매개변수를 사용하여 중간 테이블 지정
         - 두 개의 테이블이 서로 여러 개의 데이터를 가질 수 있음

 

      2. 중간 테이블을 사용한 N:M 관계 설정

         - SQLAlchemy에서는 Association Table (연결 테이블) 을 사용하여 N:M 관계를 설정한다.

from sqlalchemy import Table, Column, Integer

# 중간 테이블 (Association Table)
user_group_association = Table(
    "user_group_association", Base.metadata,
    Column("user_id", ForeignKey("user.id"), primary_key=True),
    Column("group_id", ForeignKey("group.id"), primary_key=True)
)

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    groups: Mapped[list["Group"]] = relationship("Group", secondary=user_group_association, back_populates="users")

class Group(Base):
    __tablename__ = "group"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False, unique=True)

    users: Mapped[list["User"]] = relationship("User", secondary=user_group_association, back_populates="groups")

 

      3. N:M 관계 설정 핵심

         - user_group_association라는 중간 테이블을 생성하여 User와 Group을 연결
         - relationship(..., secondary=user_group_association)을 사용하여 다대다 관계를 설정
         - back_populates를 사용하여 양방향으로 접근 가능

 

      4. DynamicMapped를 사용한 N:M 관계

         - N:M 관계에서도 대량의 데이터를 관리할 때는 DynamicMapped를 사용하는 것이 좋다.

         - user.groups.all() 또는 user.groups.filter(Group.name == "Admin").all()처럼 동적으로 데이터 로드 가능하다.

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    groups: DynamicMapped["Group"] = relationship("Group", secondary=user_group_association, back_populates="users", lazy="dynamic")

class Group(Base):
    __tablename__ = "group"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False, unique=True)

    users: DynamicMapped["User"] = relationship("User", secondary=user_group_association, back_populates="groups", lazy="dynamic")

 

6. uselist 옵션에 대한 설명 추가

   - uselist는 SQLAlchemy의 relationship()에서 사용되는 옵션으로, 관계된 객체를 리스트로 반환할지 여부를 결정하는 역할을 한다.

 

   - uselist=True (기본값)

      - 다대일(1:N) 또는 다대다(N:M) 관계에서 기본적으로 리스트(list[T]) 형태로 데이터를 반환한다.
      - 즉, relationship()을 사용할 때 기본적으로 uselist=True이며, 이는 여러 개의 객체를 포함할 수 있음을 의미한다.

 

   - uselist=False

      - 1:1(One-to-One) 관계에서 사용되며, relationship()이 단일 객체를 반환하도록 강제한다.
      - ForeignKey 컬럼에 unique=True가 설정되어 있어야 한다.

 

   1) uselist 옵션 사용 예제

from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    profile: Mapped["UserProfile"] = relationship("UserProfile", back_populates="user", uselist=False)  # 1:1 관계

class UserProfile(Base):
    __tablename__ = "user_profile"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True)  # 반드시 unique=True 설정
    bio: Mapped[str] = mapped_column(nullable=True)

    user: Mapped["User"] = relationship("User", back_populates="profile")

 

   2) 1:1 관계에서 uselist=False가 필요한 이유

      - User.profile이 단일 객체(UserProfile)로 반환됨
      - Foreign Key에 unique=True를 설정하여 한 개의 User가 하나의 UserProfile만 가질 수 있도록 제한
      - 만약 uselist=True라면 User.profile이 리스트(list[UserProfile])가 되어버림 (1:1 관계가 아닌 1:N처럼 동작)\

 

   3) uselist=True를 사용한 1:N 관계 (기본값)

      - User.posts는 여러 개의 Post를 가질 수 있기 때문에 리스트(list[Post])로 반환됨

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(nullable=False, unique=True)

    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")  # 기본적으로 uselist=True

class Post(Base):
    __tablename__ = "post"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(nullable=False)
    content: Mapped[str] = mapped_column(nullable=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))

    author: Mapped["User"] = relationship("User", back_populates="posts")

 

   4) 정리

      - uselist는 관계의 유형을 명확하게 설정하는 중요한 옵션이며, 1:1 관계에서는 uselist=False를 반드시 설정해야 한다.

uselist 값 사용 예제 반환되는 데이터 타입 사용 이유
uselist=True (기본값) 1:N, N:M 관계 (User.posts, User.groups) list[T] (리스트) 다수의 객체를 포함해야 할 때
uselist=False 1:1 관계 (User.profile) T (단일 객체) 단일 객체만 반환해야 할 때 (Foreign Key에 unique=True 필요)

댓글