[Python] SQLAlchemy에서 다대다 관계 설정 시 발생하는 경고 해결 방법

JUNYOUNG·2024년 12월 11일
post-thumbnail

도입

다대다(N:M) 관계는 데이터베이스 설계에서 자주 등장하는 패턴 중 하나다. 예를 들어, 한 명의 사용자가 여러 개의 프로모션을 가질 수 있고, 하나의 프로모션은 여러 명의 사용자에게 제공될 수 있다. 이번 프로젝트에서는 UserPromotion 간의 다대다 관계를 설정하는 과정에서 새로운 요구사항을 반영해야 했고, 이로 인해 설계가 변경되었다.

초기 설계

프로젝트의 초기 설계는 다음과 같았다:

  1. User-Promotion 관계
    • 한 명의 유저는 여러 개의 프로모션을 가질 수 있다.
    • 하나의 프로모션은 여러 명의 유저에게 제공될 수 있다.
  2. Promotion-Firm 관계
    • 프로모션은 특정 기업(Firm)이 발행한다.

초기 설계는 다대다 관계를 간단히 처리하기 위해 SQLAlchemy의 secondary 옵션을 사용했다. 중간 테이블은 암묵적으로 설정되었으며, 별도의 모델로 정의되지 않았다.

코드는 다음과 같았다:

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    username = Column(String(80), nullable=False)

    promotions = relationship(
        "Promotion",
        secondary="user_promotion",  # 암묵적인 중간 테이블
        back_populates="users"
    )

class Promotion(Base):
    __tablename__ = "promotion"

    id = Column(Integer, primary_key=True)
    code = Column(String(50), unique=True, nullable=False)

    users = relationship(
        "User",
        secondary="user_promotion",  # 암묵적인 중간 테이블
        back_populates="promotions"
    )

이 설계는 단순한 다대다 관계를 효과적으로 처리할 수 있었으나, 프로젝트가 진행되면서 새로운 요구사항이 추가되었다.


추가된 요구사항

추가된 요구사항은 다음과 같았다:

  • 특정 프로모션을 가진 유저 중 악의적인 유저가 있을 경우, 해당 유저의 프로모션 사용 권한을 비활성화해야 했다.
  • 이를 위해 각 유저-프로모션 관계의 활성 상태(is_valid)를 관리할 필요가 있었다.

이 요구사항을 반영하기 위해 기존의 secondary 방식으로는 충분하지 않았다. 관계의 추가적인 정보를 관리하려면 명시적인 중간 테이블이 필요했기 때문이다.

설계 변경

새로운 요구사항을 처리하기 위해, 기존의 secondary 방식을 대체하여 중간 테이블을 명시적으로 정의했다.

이 설계를 통해 유저-프로모션 관계에 is_valid 필드를 추가하고, 이를 기반으로 유저의 프로모션 사용 권한을 관리할 수 있었다.

문제와 해결

문제: 경고 메시지 발생

설계를 변경한 후, SQLAlchemy에서 다음과 같은 경고 메시지가 발생했다:

relationship 'Promotion.users' will copy column promotion.id to column user_promotion.promotion_id, which conflicts with relationship(s): 'Promotion.user_promotions'

이 문제는 같은 외래 키(promotion_id, user_id)를 참조하는 중복된 관계 정의로 인해 발생했다. SQLAlchemy는 어떤 관계를 기준으로 데이터를 조작해야 할지 명확히 알 수 없었다.


해결 방법

  1. viewonly=True 옵션 추가

    User.promotions와 Promotion.users 관계에 viewonly=True를 설정하여 읽기 전용 관계로 처리했다.

    이를 통해 데이터 수정 시 발생할 수 있는 충돌을 방지했다.

  2. 외래 키 명시

    UserPromotion에서 각 관계에 외래 키를 명시적으로 지정했다.

    SQLAlchemy가 올바르게 키를 매핑하도록 하여 중복 참조 문제를 해결했다.


최종 코드

최종적으로 경고 메시지를 해결하고 요구사항을 충족한 코드는 다음과 같다:

UserPromotion 모델

class UserPromotion(Base):
    __tablename__ = "user_promotion"

    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
    promotion_id = Column(Integer, ForeignKey("promotion.id", ondelete="CASCADE"), primary_key=True)
    is_valid = Column(Boolean, default=True, nullable=False)

    user = relationship("User", back_populates="user_promotions", foreign_keys=[user_id])
    promotion = relationship("Promotion", back_populates="user_promotions", foreign_keys=[promotion_id])

User 모델

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    username = Column(String(80), nullable=False)

    # UserPromotion과의 관계
    user_promotions = relationship("UserPromotion", back_populates="user")

    # 읽기 전용 다대다 관계
    promotions = relationship(
        "Promotion",
        secondary="user_promotion",
        back_populates="users",
        viewonly=True
    )

Promotion 모델

class Promotion(Base):
    __tablename__ = "promotion"

    id = Column(Integer, primary_key=True)
    code = Column(String(50), unique=True, nullable=False)

    # UserPromotion과의 관계
    user_promotions = relationship("UserPromotion", back_populates="promotion")

    # 읽기 전용 다대다 관계
    users = relationship(
        "User",
        secondary="user_promotion",
        back_populates="promotions",
        viewonly=True
    )

결론

이번 설계를 통해 다대다 관계에서 중간 테이블을 활용하여 추가 요구사항을 처리하는 방법을 배울 수 있었다.

  • 요구사항 충족: is_valid 필드를 통해 사용자-프로모션 관계의 활성 상태를 관리.
  • 경고 해결: viewonly=True와 overlaps 옵션을 통해 중복된 관계 정의로 인한 충돌 문제 해결.
  • 유연성 확보: 명시적인 중간 테이블 정의로 확장 가능한 설계 구조 구현.

이번 사례는 ORM을 활용할 때 초기 설계의 단순함과 새로운 요구사항 간의 균형을 맞추는 것이 얼마나 중요한지 보여준다. 특히, 다대다 관계를 ORM으로 처리할 때 관계의 명확성을 유지하면서도 확장성을 고려해야 한다는 점을 다시금 느낄 수 있었다.

추가로, 실무에서 지인이나 주변 개발자들로부터 들었던 흥미로운 점도 떠올랐다. ORM은 추가, 수정, 삭제와 같은 작업을 처리할 때 매우 유용하지만, 복잡한 조회 작업에서는 연관 관계의 복잡성과 성능 문제로 인해 SQL을 직접 사용하는 경우가 많다는 것이다. 특히, 여러 관계를 조인하거나 필터링이 필요한 경우, 직접 SQL을 작성하는 것이 더 직관적이고 성능 면에서 유리할 때가 많다.

이러한 점은 ORM의 강점과 한계를 이해하고, 상황에 따라 적절한 도구를 선택하는 것이 얼마나 중요한지를 깨닫게 한다. 앞으로도 설계와 구현에서 명확성과 유연성, 그리고 성능을 모두 고려하며 더 나은 시스템을 구축하기 위해 노력할 것이다.



viewonly 개념


viewonly=True

개념

viewonly=True는 관계를 읽기 전용으로 설정하는 옵션이다. 이 옵션을 사용하면 ORM이 해당 관계를 통해 데이터를 수정하거나 추가하지 못하도록 한다.

왜 사용하는가?

  • 데이터 조회만 필요한 관계를 명확히 정의하기 위해 사용된다.
  • 데이터 수정 충돌을 방지하고, 데이터베이스와의 일관성을 유지할 수 있다.
  • 중간 테이블이 이미 다른 관계를 통해 관리되고 있어, 중복된 수정이 발생하지 않도록 한다.

어떻게 사용하는가?

관계를 정의할 때 viewonly=True를 추가한다. 이를 통해 SQLAlchemy는 해당 관계를 수정 가능한 관계로 처리하지 않고, 조회 전용으로 처리한다.

언제 사용하는가?

  • 관계가 조회만 필요하고, 데이터베이스에 영향을 주지 않아야 할 때.
  • 복잡한 연관 관계에서 일부 관계를 읽기 전용으로 처리하여 설계를 단순화하고 충돌을 방지해야 할 때.

장점

  • 데이터를 수정하지 않으므로 불필요한 충돌을 방지한다.
  • 조회 작업의 명확성을 높이고 설계를 단순화한다.

단점

  • 수정이 필요한 경우 사용할 수 없다.
  • 관계가 복잡해질 경우 viewonly 설정만으로는 모든 문제를 해결하지 못할 수 있다.

결론

viewonly=True는 상황에서 유용하다:

  • viewonly=True는 데이터 조회만 필요한 관계에서 수정 충돌을 방지하고 설계를 단순화한다.
  • overlaps는 중복된 외래 키 참조로 발생하는 충돌을 명확히 정의하고 방지한다.

이 두 옵션은 ORM 설계의 명확성과 데이터 일관성을 유지하기 위해 중요한 도구다. 적절히 활용하면 복잡한 데이터 관계에서도 성능과 안정성을 모두 확보할 수 있다.

Ref.
https://docs.sqlalchemy.org/en/20/orm/relationships.html

profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글