1. FK를 사용하는 경우
"데이터 무결성을 DB 레벨에서 강제하여, 애플리케이션의 실수로부터 데이터를 보호하는 것"에 초점을 맞추는, 가장 전통적이고 안전한 전략.
장점 (Pros)
1. 압도적인 데이터 무결성 보장 (The Fortress)
이것이 FK를 사용하는 가장 큰 이유이다. DB는 '데이터의 최종 수호자' 역할을 한다.
- '고아 데이터(Orphaned Record)' 원천 차단:
- 예시: users (부모 테이블)와 orders (자식 테이블)가 있다고 가정해 보자. orders 테이블의 user_id 컬럼은 users.id를 FK로 참조한다.
- 상황: 만약 개발자가 실수로 user_id = 999 (존재하지 않는 유령 회원)의 주문 데이터를 INSERT하려고 시도하면, DB는 FOREIGN KEY constraint fails 오류를 발생시키며 INSERT 자체를 거부한다.
- 결과: 데이터베이스에는 절대로 부모 없는 유효하지 않은 데이터가 쌓이지 않는다.
- '동시성 문제(Race Condition)'로부터의 보호:
- 상황: FK가 없다면, 애플리케이션 코드로 INSERT 직전에 SELECT를 통해 user_id=100이 존재하는지 검사해야 한다.
- 문제:
- Thread 1: user_id=100 회원이 users 테이블에 있는지 확인 (결과: 있음)
- Thread 2: (동시에) 다른 관리자가 user_id=100 회원을 탈퇴(DELETE)시킴
- Thread 1: user_id=100이 있다고 확인했으므로, orders 테이블에 INSERT 시도
- 결과: FK가 없다면 이 INSERT는 성공하고, DB에는 '고아 데이터'가 쌓인다. 하지만 FK가 있었다면 DB가 3번 단계에서 INSERT를 막아주어 데이터 무결성을 지킬 수 있다.
2. 명확한 설계 문서 (The Blueprint)
FK는 그 자체로 살아있는 '설계 문서'다.
- 관계의 명시성: ERD(Entity-Relationship Diagram) 툴이 DB 스키마를 읽어올 때, FK 관계가 모두 선으로 표시된다. 신입 개발자가 프로젝트에 투입되어도, orders가 users와 products에 의존한다는 사실을 ERD만 보고도 즉시 파악할 수 있다.
- 신뢰성: 주석이나 별도 위키(Wiki) 문서는 시간이 지나며 실제 코드와 달라질 수(decay) 있지만, DB 스키마에 정의된 FK는 변하지 않는다.
3. 개발 실수 방지 및 로직 강제 (The Safety Net)
FK는 개발자가 비즈니스 로직을 더 깊게 고민하도록 '강제'한다.
- 예시: users 테이블을 DELETE 하려고 할 때, 이 유저를 참조하는 orders 데이터가 있다면 어떻게 해야 할까요?
- ON DELETE RESTRICT (기본값): 자식(orders)이 존재하면 부모(users) 삭제를 막는다. -> "아, 탈퇴시키기 전에 주문 내역부터 처리해야 하는구나!"
- ON DELETE CASCADE: 부모(users)가 삭제되면 자식(orders)도 연쇄적으로 자동 삭제한다.
- ON DELETE SET NULL: 부모(users)가 삭제되면 자식(orders)의 user_id를 NULL로 변경한다.
- 결과: 이 옵션을 선택하는 과정 자체가 "회원 탈퇴 시 주문 데이터는 어떻게 처리할 것인가?"라는 핵심 비즈니스 로직을 설계하는 행위이다.
단점 (Cons)
1. 쓰기 성능 저하 (The "Tax")
무결성을 '검사'하는 데는 공짜가 없다.
- 오버헤드 발생: INSERT나 UPDATE를 할 때마다, DB는 참조하려는 부모 테이블에 해당 데이터가 '실제로 존재하는지' 확인(Lookup)하는 추가 작업을 수행한다.
- '성능 세금': 이 오버헤드는 보통 5~10% 내외의 성능 저하를 유발하며, 전문가들은 이를 '데이터 무결성을 위한 합리적인 세금'이라고 부른다.
- (치명적) 인덱스 누락 시 재앙:
- 상황: users (부모)를 DELETE 할 때, orders (자식) 테이블의 user_id (FK 컬럼)에 인덱스가 없다면 어떻게 될까요?
- 결과: DB는 이 users를 참조하는 orders가 있는지 확인하기 위해, orders 테이블 전체(수억 건)를 풀 스캔(Full Table Scan)합니다. 이로 인해 DELETE 쿼리 하나가 몇 시간씩 걸리거나 DB 전체를 마비시킬 수 있습니다. (이는 FK 자체의 문제라기보단, DB 설계/운영의 미숙함이다.)
2. 유지보수 및 마이그레이션의 복잡성 (The "Handcuffs")
서비스가 초대규모로 성장했을 때, 이 '안전벨트'가 '족쇄'가 됩니다.
- 예시: users 테이블의 PK인 id를 INT (21억)에서 BIGINT (900경)로 변경해야 한다고 가정해 봅시다.
- 작업 순서 (지옥):
- users.id를 참조하는 모든 자식 테이블(orders, comments, user_profiles...)을 찾는다.
- 서비스를 점검(중단)한다.
- 모든 자식 테이블의 FOREIGN KEY ... 제약조건을 전부 DROP한다. (이 작업은 Lock을 유발.)
- users.id와 모든 자식 테이블의 user_id 컬럼 타입을 BIGINT로 변경한다. (수십억 건 데이터 변경은 몇 시간이 걸릴 수 있다.)
- 모든 자식 테이블에 FOREIGN KEY ... 제약조건을 전부 다시 ADD 한다. (이 작업 역시 Lock을 유발하며 기존 데이터를 모두 검증.)
- 결과: gh-ost나 pg_repack 같은 무중단 마이그레이션 도구가 이 FK 때문에 무력화되며, 장시간의 서비스 중단이 필요할 수 있다.
3. 개발 초기 단계의 번거로움 (The "Hurdle")
신입 개발자들이 귀찮아 하는 부분.
- 삽입 순서 강제: 테스트 코드나 data.sql에 더미 데이터를 넣을 때, orders 데이터를 먼저 넣으려고 하면 100% 오류가 발생한다.
- 해결: 반드시 users와 products (부모)를 먼저 INSERT하고, 그다음 orders (자식)를 INSERT해야 한다. 이 의존성 순서를 파악하고 지키는 것이 매우 번거롭다.
2. FK를 사용하지 않는 경우 (The "Performance-First" Strategy)
"DB의 제약은 최소화하고, 모든 무결성 책임을 애플리케이션이 지며, 최고의 쓰기 성능과 확장성을 확보하는 것"에 초점을 맞추는, 하이퍼스케일 서비스(Hyperscale) 지향 전략.
장점 (Pros)
1. 최고의 쓰기 성능 (The Superhighway)
DB가 '검사'를 하지 않으니, 당연히 빠르다.
- '성능 세금' 면제: INSERT/UPDATE 시 부모 테이블을 확인하는 오버헤드가 완전히 사라진다.
- '성능 절벽(Performance Cliff)' 회피:
- 상황: 1개의 부모 로우(예: 방송)에 수백만 개의 자식 로우(예: 시청자)가 동시에 INSERT될 때, FK가 있다면 이 1개의 부모 로우에 극심한 Lock 경합이 발생해 시스템 전체가 멈춘다.
- 결과: FK가 없다면, 부모 로우를 쳐다볼 필요도 없으므로 Lock 자체가 발생하지 않는다. orders 테이블은 그저 user_id=100이라는 '숫자'를 저장할 뿐, users 테이블과는 아무런 관계가 없다.
2. 자유로운 스키마 변경 및 유지보수 (The Freedom)
테이블 간의 물리적 연결(족쇄)이 없으므로, 마이그레이션이 자유롭다.
- 예시: 위에서 든 INT -> BIGINT 변경 작업을 다시 생각해 보자.
- 작업 순서 (천국):
- users 테이블의 id 타입을 BIGINT로 변경. (온라인 마이그레이션)
- orders 테이블의 user_id 타입을 BIGINT로 변경. (온라인 마이그레이션)
- 결과: 두 작업은 서로 아무런 의존성이 없으므로, 서비스 중단 없이 각각 독립적으로, 안전하게 수행할 수 있다.
3. 하이퍼스케일 확장성 (The Scalability)
이것이 하이퍼스케일 기업이 FK를 포기하는 궁극적인 이유다.
- 샤딩(Sharding) 가능: 데이터가 너무 많아 DB를 여러 대로 분산(샤딩)해야 할 때, users 테이블은 1번 서버에, orders 테이블은 2번 서버에 저장될 수 있다.
- 물리적 한계: "네트워크 케이블을 넘어가는 FK"는 존재할 수 없습니다. 2번 서버의 DB는 1번 서버의 users 테이블을 참조할 방법이 없습니다. 따라서 샤딩을 하기 위해서는 FK를 반드시 제거해야 한다.
단점 (Cons)
1. 데이터 오염 위험 (The Minefield)
모든 책임은 애플리케이션에 있다. DB는 이제 '단순한 저장소(Storage)'일 뿐이다.
- 버그 = 데이터 오염: 앞서 설명한 '동시성 문제'나 '개발자 실수'로 '고아 데이터'가 INSERT되면, DB는 이를 전혀 막아주지 못한다.
- 책임 전가: 데이터가 오염되었을 때, 이는 DB의 잘못이 아니라 100% 애플리케이션 코드의 버그가 된다.
2. 애플리케이션 복잡도 급증 (The Burden)
"데이터베이스가 공짜로 해주던 모든 일을, 이제 애플리케이션이 직접 해야 한다."
- 무결성 검증 코드: 모든 INSERT 로직에 SELECT로 부모가 있는지 확인하는 코드를 개발자가 직접 구현해야 한다.
- 연쇄 로직 구현: 회원 탈퇴 시 ON DELETE CASCADE가 없으므로, 관련 orders, comments 등을 개발자가 코드로 직접 찾아 DELETE해야 합니다. 만약 comments를 빠뜨린다면? 그 데이터는 영원히 '고아 데이터'로 DB에 남게 된다.
- 데이터 클렌징 배치: 버그는 반드시 발생하기 때문에, 주기적으로 DB를 스캔하여 '고아 데이터'를 찾아 삭제하는 별도의 정리(Cleanup) 배치 프로그램을 만들어야 한다. 이는 엄청난 엔지니어링 비용이다.
3. 불명확한 스키마 (The Fog)
스키마만 봐서는 아무것도 알 수 없다.
- 관계의 실종: orders 테이블의 user_id 컬럼이 users.id를 참조하는지, admins.id를 참조하는지, 혹은 그냥 다른 서비스의 UUID인지 DB 스키마만 봐서는 절대 알 수 없다.
- 높은 유지보수 비용: 관계를 파악하려면 애플리케이션의 코드 전체를 뒤져봐야 합니다. 이는 신규 입사자의 적응을 매우 어렵게 만들고, 디버깅 시간을 폭발적으로 증가시킨다.
3. 결론 | 핵심 트레이드오프: 그래서 우리는 무엇을 선택해야 하는가?
FK 사용 여부는 "지금 우리 서비스의 단계에서 무엇이 더 중요한가?"라는 트레이드오프 문제.
- [서비스 초기] : FK 사용
- 상황: 사용자가 적고, 팀이 작으며, 버그가 많다.
- 트레이드오프: '사소한 쓰기 성능(10%)'과 '개발 초기 세팅의 번거로움'을 포기(Cost)하고, '압도적인 데이터 안정성'과 '명확한 설계'라는 이득(Benefit)을 취한다.
- 결론: 이 단계에서 하이퍼스케일을 걱정하며 FK를 쓰지 않는 것은 '시기상조의 최적화'이다.
- [하이퍼스케일] : FK 제거 고려
- 상황: 트래픽이 폭발하여 '성능 절벽'이나 '샤딩 불가'가 서비스의 생존을 위협한다.
- 트레이드오F프: 'DB가 보장하는 무결성'이라는 편의성을 포기(Cost)하는 대신, '막대한 엔지니어링 비용(복잡한 코드, 배치 개발)'을 지불(Cost)하고, '최고의 성능과 확장성'이라는 이득(Benefit)을 취한다.
- 결론: FK를 제거하는 것은 '좋아서'가 아니라, 생존을 위해 어쩔 수 없이 감수하는 비싼 비용이다.
참고 링크
Not using foreign key constraints in real practice. Is it OK?
Not using FK constraints is my company's untold rule. FK constraints are used only when designing ERD and not used when creating tables. According to my senior, in real practice, those are very time
dba.stackexchange.com
https://blog.naver.com/hist0134/220249120040
FK를 쓰지 않는 이유
나 : "음.. 그럼 이 컬럼에는 FK를 걸어야겠네요" CTO : "아냐, 하지마. FK는 아무렇게나...
blog.naver.com
https://github.com/github/gh-ost/issues/331
Thoughts on Foreign Keys? · Issue #331 · github/gh-ost
Hello, Thanks for your hard work on gh-ost! As I familiarize myself with the way it all works, I noted that foreign keys are explicitly not supported, but that they may be to some extent in the fut...
github.com
https://postgres.fm/episodes/should-we-use-foreign-keys
Postgres FM | Should we use foreign keys?
share.transistor.fm
'프로젝트 > 회고' 카테고리의 다른 글
| @OneToOne 관계 매핑, 세가지 설계 방식과 그에 따른 Trade-off (0) | 2025.11.08 |
|---|