“모든 테이블을 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_id가 users 테이블의 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는 반드시 두는 것이 좋습니다.