본문 바로가기

Case Study/Trouble Shooting

FK는 왜 걸까? 실무 에러로 배우는 DB 제약 조건

포스팅 전에 앞서,

왜 난 이 주제의 포스팅을 생각했을까? 😐

 

원래 FK를 잘 걸지 않는 편인데 왜냐하면 운영 시 실제 데이터 값을 직접 변경해 줘야 하는 이슈도 생겨 FK 걸린 테이블 내 모든 정보를 수정해 줘야 하는 상황으로 번지고, 하나의 row 를 삭제 시 FK로 물려있어 제약이 많이 걸리기에 운영할 때의 불편함 이슈로 지금껏 대부분 FK를 걸지 않고 테이블 설계를 진행

 

그러다 이직 후, 협업 중 ERD 설계할 때 FK 조건을 걸게 되는 상황에 놓이게 되어 FK? 너 왜 이렇게 걸리는건데? 싶어 생각 정리 겸 포스팅을 하게 되었습니다. 


FK(Foreign Key) 란?

관계 무결성을 DB 레벨에서 보장해주는 키

 

FK의 장단점

장점

1. 데이터 무결성 보장

 

  • 부모 테이블에 없는 ID가 자식 테이블에 들어오는 것을 원천 차단
  • 잘못된 데이터가 쌓이는 것을 DB 수준에서 막아주기 때문에 데이터 신뢰도 상승

 

2. 데이터 일관성

  • 부모 데이터가 삭제되거나 수정될 때, 연결된 자식 데이터를 자동으로 지우거나(ON DELETE CASCADE) 같이 수정할 수 있어 관리 용이

3. 설계의 가독성

  • DB 스키마를 통해 테이블 간의 관계 확인 가능

단점

1. 성능 저하

  • 데이터를 입력/수정/삭제할 때마다 DB 엔진이 다른 테이블을 조회하여 확인해야하기 때문에 트래픽이 몰리는 시스템에서는 오버헤드가 누적되어 속도 저하 발생 

2. 락 및 데드락 위험

  • 부모와 자식 테이블 간의 정합성을 맞추는 과정에서 Row Lock이 발생
  • 여러 프로세스가 동시에 접근할 경우 데드락(교착 상태)이 발생할 가능성 ↑

3. 운영 및 관리 제약

 

  • 데이터 마이그레이션: 수백만 건의 데이터를 옮길 때 FK 순서를 맞추지 않으면 에러 발생
  • 더미 데이터 활용: 테스트를 위해 특정 데이터만 넣고 싶어도 부모 데이터가 먼저 있어야 하므로 개발 편의성 저하

 

4. 확장성 문제

  • DB 샤딩: 데이터를 여러 서버에 분산 저장할 경우, 물리적으로 다른 서버에 있는 테이블끼리는 FK를 걸 수 없기 때문에 MSA 구조에서는 부적합

 

실무에서 물리적 FK를 안 거는 이유 (vs 거는 이유)

실무(특히 대규모 서비스)에서는 물리적 FK를 생략하여 설계를 하는 경우가 많고 물리적으로 걸지 않더라도 논리적으로는 거는 경우도 있습니다.

물론 어떤 도메인 또는 서비스를 가지고 설계 하느냐에 따라 우선시 되어야 할 점을 명확히하고 간다면 설계 방향이 잡히게 됩니다.

하지만! 개인적으로는 물리적으로 걸진 않더라도 논리적 즉, ERD에는 표현하는 것이 좋다고 생각합니다.

그래야 이 데이터가 어디서 왔는지, 삭제될 때 같이 지워져야 하는지 등을 고려한 비즈니스 로직을 설계할 수 있기 때문입니다.

 

💭 물리적 FK를 생략하는 이유

  1. 성능(Performance): 데이터 입력/수정 시마다 매번 부모 테이블을 체크하는 비용이 증가
  2. 유연한 운영: 가끔은 테스트를 위해 데이터를 넣어야 할 때가 있는데, FK가 있으면 각 테이블에 데이터를 넣어야 하므로 편리성 저하
  3. 시스템 확장성: DB를 여러 대로 쪼개는 샤딩이나 MSA 구조에서는 물리적으로 FK를 유지 불가

 

🖊️ 실제 실무에서 만나 본 에러 정리

1. 흔한 오해: NOT NULL과 FK 에러는 다르다 ❌

ERROR: null value in column "USER_ID" violates not-null constraint

처음엔 엥? FK 때문인가? 싶었던 에러

NOT NULL 제약 조건 위반으로 생긴 에러이므로 애플리케이션 로직에서 변수에 값이 할당되지 않았거나, 매핑 실수로 null이 전달된 경우에 발생

 

2. 진짜 FK 에러: 데이터 간의 연결 확인

### SQL: INSERT INTO "TB_EAPP" ("TEMPLATE_ID", "REQUESTER_ID", ...) VALUES (?, 2026001, ...)
### Cause: org.postgresql.util.PSQLException: 오류: "TB_EAPP" 테이블에서 자료 추가 작업이 "TB_EAPP_REQUESTER_ID_fkey" 참조키 제약 조건을 위배했습니다.
Detail: (REQUESTER_ID)=(2026001) 키가 "TB_USER" 테이블에 없습니다.

 

Foreign Key 제약 조건 위반 (참조 무결성)

값은 보냈지만 그 값이 부모 테이블에 존재하지 않았을 때 발생하는 에러

보낸 유저 값이 해당 테이블에 없으니 INSERT 🚫

 

 


💡 요약 및 해결법

에러가 났다면? 먼저 부모 테이블(TB_USER)에 해당 PK 값이 실제로 있는지 쿼리 실행

데이터가 있다면? 애플리케이션에서 트랜잭션이 커밋되기 전에 자식 데이터를 먼저 넣으려 한 건 아닌가 확인

데이터가 없다면? 비즈니스 로직에서 부모 데이터를 먼저 생성하거나, 올바른 ID가 전달되도록 수정

 

데이터의 완벽한 정합성을 DB가 책임지게 할 것인가, 아니면 성능을 위해 애플리케이션 코드가 책임지게 할 것인가에 따라 FK 은 아키텍처에 따라 선택 유무가 정해지는 것 같습니다. 😅