테이블을 꼭 FK(ForeignKey)로 연결하는게 좋을까?

“모든 테이블을 ForeignKey로 연결해야 할까?”
저는 자산앱의 데이터베이스 모델링을 하면서 테이블이 늘어나고 관계가 복잡해질수록 이런 질문이 생기기 시작했습니다.
특히 FastAPI와 PostgreSQL 환경에서 SQLModel/SQLAlchemy 같은 ORM을 쓰면, 설계 결정이 코드와 런타임 동작에 직접적인 영향을 미치기 때문에 고민이 더 자주 발생하죠.
이번 글에서는 실제 예제 코드를 기반으로, FK를 사용하는 이유와 사용하지 않았을 때의 차이점, 그리고 어떤 상황에서 FK를 의도적으로 생략하는 것이 합리적인지를 살펴보겠습니다.


🧩 예제 모델 구조

아래는 자산 앱 프로젝트에서 SQLModel을 사용해 정의한 실제 모델입니다.

# models.py

class User(BaseModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    email: str
    nickname: str
    portfolios: List["Portfolio"] = Relationship(back_populates="user")

class Portfolio(BaseModel, table=True):
    __tablename__ = "portfolio"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    assets: list["Asset"] = Relationship(back_populates="portfolio")
    user_id: Optional[int] = Field(default=None, foreign_key="users.id")
    user: Optional[User] = Relationship(back_populates="portfolios")

class Asset(BaseModel, table=True):
    __tablename__ = "asset"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    category: str
    amount: float
    average_price: float = Field(default=0.0)
    portfolio_id: Optional[int] = Field(default=None, foreign_key="portfolio.id")
    portfolio: Optional[Portfolio] = Relationship(back_populates="assets")

class Deposit(BaseModel, table=True):
    __tablename__ = "deposit"
    id: Optional[int] = Field(default=None, primary_key=True)
    deposit_date: datetime = Field(sa_column=Column(DateTime))
    amount: int
    portfolio_id: Optional[int] = Field(default=None)
    memo: Optional[str] = None

여기서 주목해야 할 부분은 Portfolio와 Asset은 FK로 연결되어 있지만,
Deposit은 FK 없이 portfolio_id만 단순히 컬럼으로 존재한다는 점입니다.

🔍 ForeignKey(FK)의 기본 개념

FK란 무엇인가?

ForeignKey는 한 테이블의 특정 컬럼이 다른 테이블의 기본키(primary key)를 참조하도록 강제하는 제약조건입니다.
즉, 데이터의 참조 무결성(Referential Integrity) 을 보장하기 위한 핵심 수단입니다.

예를 들어, portfolio 테이블의 user_idusers 테이블의 id를 참조한다면,
DB는 users.id에 존재하지 않는 값이 portfolio.user_id로 들어오는 것을 막습니다.

FK를 사용하면 얻는 이점

  • 데이터 무결성 보장: 잘못된 참조(없는 user_id 등)가 저장되지 않음
  • ORM 관계 자동 관리: SQLModel이나 SQLAlchemy에서 Relationship을 통해 손쉽게 조인 가능
  • 삭제/갱신 시 연쇄 동작(CASCADE) 을 설정해 데이터 일관성 유지
  • 쿼리 가독성 향상: ORM으로 모델 간 객체 접근이 직관적 (portfolio.user.nickname 형태 등)

⚙️ FK를 생략하면 어떻게 될까?

FK를 명시하지 않아도 테이블 간 관계는 논리적으로 연결될 수 있습니다.
예를 들어, Deposit 테이블은 다음처럼 설계되어 있습니다.

class Deposit(BaseModel, table=True):
    portfolio_id: Optional[int] = Field(default=None)

이 경우에도 portfolio_id 값만 저장하면 특정 포트폴리오에 귀속된 데이터처럼 사용할 수 있습니다.
하지만 다음과 같은 문제들이 발생할 수 있습니다.

❌ 1. 참조 무결성 깨짐

portfolio_id=999 같은, 존재하지 않는 포트폴리오 ID를 저장해도 DB는 막지 않습니다.
즉, 애플리케이션 로직에서 별도로 검증하지 않으면 데이터 불일치가 생깁니다.

❌ 2. ORM 관계 불가

ForeignKey가 없으면 ORM에서 자동으로 관계를 추적할 수 없습니다.
따라서 다음과 같은 표현은 불가능합니다.

deposit.portfolio.name  # ❌ FK 없으면 불가능

이 경우 매번 수동으로 조인 쿼리를 작성해야 합니다.

❌ 3. 삭제 시 고아 데이터 발생

portfolio가 삭제되더라도 해당 포트폴리오에 연결된 deposit 레코드는 남습니다.
이로 인해 “고아 데이터(orphan data)”가 누적되어 데이터 정합성이 떨어집니다.


⚖️ 그렇다면 왜 FK를 생략할까?

FK는 이상적인 제약 조건이지만, 실무에서는 무조건 FK를 사용하는 것이 항상 정답은 아닙니다.
다음과 같은 이유로 FK를 의도적으로 생략하는 경우도 있습니다.

✅ 1. 마이크로서비스나 분산 시스템 환경

DB가 여러 서비스로 분리되어 있을 경우, FK 제약은 서비스 간 결합도를 높입니다.
예를 들어, User DB와 Portfolio DB가 다른 인스턴스에 있다면 FK 제약은 불가능합니다.
이 경우 논리적 FK(소프트 FK) 만 유지하고, 데이터 정합성은 API 레벨에서 관리합니다.

✅ 2. 성능 최적화 목적

대규모 트랜잭션이 발생하는 테이블에서 FK 제약을 두면,
INSERT/UPDATE 시마다 참조 무결성 검사 비용이 추가됩니다.
금융권이나 로그성 데이터처럼 트래픽이 많은 환경에서는 FK를 제거하고 애플리케이션 단에서 검증하는 방식을 택하기도 합니다.

✅ 3. 배포 및 마이그레이션 단순화

FK 제약이 많을수록 스키마 변경 시 충돌 가능성이 커집니다.
예를 들어 부모 테이블을 삭제하거나 PK를 변경해야 할 경우 FK가 있으면 제약 해제가 필요합니다.
따라서 데이터베이스 마이그레이션(Alembic 등) 시 유연성을 위해 FK를 생략하기도 합니다.


💡 FK를 사용할 때의 팁

1️⃣ 관계는 양방향으로 설정

ORM에서는 Relationship(back_populates=...) 를 통해 양방향 관계를 명시하면 데이터 접근이 훨씬 직관적입니다.

class Portfolio(BaseModel, table=True):
    assets: list["Asset"] = Relationship(back_populates="portfolio")

class Asset(BaseModel, table=True):
    portfolio: Optional[Portfolio] = Relationship(back_populates="assets")

2️⃣ FK 이름은 명확하게

관계 명이 많아질수록 컬럼 이름(user_id, portfolio_id)은 일관성 있게 유지해야 합니다.
SQLAlchemy는 문자열 기반으로 FK를 지정하므로, 오타가 있으면 런타임 에러가 발생할 수 있습니다.

3️⃣ FK와 Index 병행 사용

FK 컬럼에 인덱스를 걸면 조인 성능을 향상시킬 수 있습니다.

CREATE INDEX idx_portfolio_user_id ON portfolio(user_id);

🧭 결론: “FK는 도구이지, 규칙이 아니다”

정리하자면,
FK는 데이터 무결성과 ORM 편의성을 보장하는 훌륭한 도구지만,
모든 상황에서 반드시 사용해야 하는 것은 아닙니다.

상황FK 사용 권장FK 생략 고려
내부 시스템, 단일 DB✅ 필수
대규모 트래픽, 로그성 데이터⚠️ 선택적✅ 가능
분산 DB / 마이크로서비스❌ 불가✅ 논리적 FK 사용
외부 시스템 연계⚠️ 제한적✅ 선호

결국 중요한 것은 데이터 무결성을 어떤 계층에서 관리할 것인가 입니다.
애플리케이션 코드에서 충분히 검증 로직을 구현할 수 있다면 FK를 생략할 수도 있습니다.
반면, 데이터 정합성이 핵심인 서비스(금융, 계약, 거래 등)라면 FK는 반드시 두는 것이 좋습니다.


참고

댓글 남기기