외래키를 사용하지 않은 DB 기반 서비스 개발
최근 CQRS, GitOps, Cloud Native 방식등이 대두되고, 철저한 정합성 보다는 다른 이득이 더 중요하다고 판단하는 많은 서비스사에서 외래키를 사용하는 것을 지양하는 방식으로 개발 되고 있습니다.
프로라면 항상 관점을 달리 바라보는게 필요합니다. 예를들어 Controller-Service-Repository 구조라든지 Entity,VO,DAO,DTO 같은 것도 정답이 있는게 아닙니다. Controller - Repository 라고 하는게 틀린게 아니란 말입니다.
FK를 생략함으로써 얻을 수 있는 장점은 다음과 같습니다.
✅ FK 없이 설계했을 때의 장점
1️⃣ 성능 향상 (쓰기 작업 최적화)
INSERT/UPDATE 속도 향상
FK가 있으면 부모 테이블의 존재 여부를 확인하는 과정이 필요해서 추가적인 조회 비용이 발생함.
FK를 제거하면 바로 데이터 입력 가능해서 대량 삽입(ETL, 데이터 마이그레이션)이 빠름.
DELETE 성능 향상
FK가 걸려 있으면 삭제 전에 참조하는 자식 레코드를 먼저 삭제해야 해서 성능이 저하됨.
FK가 없으면 부모-자식 관계를 강제하지 않기 때문에 단일 DELETE가 가능함.
2️⃣ 수직적 확장(Sharding) 용이
FK가 걸려 있으면 분산 DB(Sharding)에서 관리가 어려움.
특정 테이블이 다른 노드(DB 서버)에 있을 경우 FK 검사를 위해 네트워크 요청이 필요할 수도 있음.
FK 없이 애플리케이션 레벨에서 참조 관계를 관리하면 DB 분산이 쉬워짐.
3️⃣ 데이터 마이그레이션이 쉬움
FK가 있으면 테이블 간 의존성이 강해서 데이터 이동이 복잡함.
FK가 없으면 테이블을 개별적으로 백업 및 복원 가능.
기존 데이터를 새로운 시스템으로 이동할 때 순서 없이 적재 가능.
4️⃣ 더 자유로운 스키마 변경
FK가 있으면 부모 테이블을 변경하려면 자식 테이블도 같이 변경해야 하는 경우가 많음.
FK가 없으면 각 테이블을 독립적으로 변경할 수 있어서 유연한 데이터 구조 변경 가능.
테이블을 쪼개거나 합칠 때 FK 때문에 의존성이 생기는 문제를 방지할 수 있음.
5️⃣ 애플리케이션 레벨에서 관계를 유연하게 관리 가능
FK 없이 UUID, 해시 값, 자연 키(Natural Key) 등으로 관계를 정의 가능.
특정 조건에서 일부 데이터만 특정 서비스에서 관리할 수도 있음.
예를 들어, CQRS 패턴(Command Query Responsibility Segregation)에서는 쓰기와 읽기 모델이 분리되므로 FK가 필요 없을 수도 있음.
그리고 FK 없는 설계가 JPA나 Exposed 개발에서 연관관계 매핑을 단순화하는 데 도움이 될 수 있어.
특히 도메인 모델이 복잡한 경우나 트랜잭션 성능을 최적화하려는 경우에 유용할 수 있지.
✅ GitOps & 클라우드 네이티브 설계에서 FK 없는 설계가 적합한 이유
1️⃣ 마이크로서비스에서 서비스 간 독립성을 보장
클라우드 네이티브 환경에서는 마이크로서비스가 서로 독립적으로 배포되고 운영되어야 해.
FK가 있으면 서비스 간 강한 결합이 발생 → 하나의 서비스가 변경되면 다른 서비스에도 영향을 줌.
FK 없이 ID만 저장하면 서비스 간 독립성 유지 → API 호출이나 이벤트 기반 방식으로 참조.
🔍 예제: FK 없이 ID만 저장하는 방식 (마이크로서비스 친화적)
@Entity
class Order(
@Id @GeneratedValue
val id: Long = 0,
val userId: Long // FK 없이 ID만 저장
)
userId를 저장하고, 필요하면 다른 서비스에서 API 호출로 사용자 정보 가져옴.FK가 없으므로 DB 수준에서 다른 서비스에 종속되지 않음.
📌 이점:
서비스별 독립적 스케일링 가능 (User 서비스와 Order 서비스 분리)
배포 순서에 덜 의존적 (FK가 있으면 DB 마이그레이션이 어렵고, 배포 순서가 중요해짐)
2️⃣ GitOps에서 인프라 & 데이터베이스 관리가 유연해짐
GitOps는 모든 인프라를 코드(IaC)로 관리하는 방식이야.
FK가 있으면 데이터베이스 변경이 까다로움 → 마이그레이션이 복잡해짐.
FK 없이 ID만 저장하면 DB 변경이 독립적 → 배포와 DB 스키마 변경이 더 유연해짐.
🔍 예제: GitOps + FK 없는 설계 활용
FK를 사용하면 배포할 때 마이그레이션 타이밍이 중요함.
User테이블이 먼저 변경되어야Order테이블의 FK가 깨지지 않음.GitOps에서 여러 서비스가 동시에 배포되면 FK 관계가 깨질 수 있음.
FK 없이
userId만 저장하면 서비스가 독립적으로 배포 가능.Order 서비스가 배포될 때 User 서비스가 배포되지 않아도 영향 없음.
📌 이점:
데이터 마이그레이션이 간편해짐 (GitOps 환경에서 자동화 배포가 쉬워짐)
DB 변경 시 서비스 배포 순서를 덜 신경 써도 됨
3️⃣ 클라우드 네이티브에서 데이터 분산 & 글로벌 배포 최적화
클라우드 네이티브 시스템에서는 멀티 리전, 멀티 클러스터 환경을 많이 사용해.
FK가 있으면 데이터가 단일 DB에 종속됨 → 글로벌 확장이 어려움.
FK 없이 ID만 저장하면 서비스 간 데이터 분산이 쉬움 → 멀티 리전 배포 최적화.
🔍 예제: 데이터 분산을 고려한 설계
AWS DynamoDB, Google Spanner 같은 분산 DB에서는 FK를 사용하지 않음.
각 서비스별로 데이터를 저장하고 필요할 때만 API 호출 또는 메시지 큐(Kafka, SQS)로 연계.
📌 이점:
멀티 리전 배포 최적화 (각 서비스가 별도 DB를 가질 수 있음)
레거시 DB 종속성 제거 (RDBMS FK 대신 NoSQL, 분산 DB 사용 가능)
4️⃣ 비동기 & 이벤트 기반 아키텍처와 잘 맞음
GitOps & 클라우드 네이티브 환경에서는 비동기 이벤트 기반 아키텍처를 많이 써.
FK가 있으면 동기 호출이 필요함 → 성능과 확장성이 떨어짐.
FK 없이 ID만 저장하면 이벤트 기반으로 연관 데이터를 조회 가능.
🔍 예제: Kafka를 활용한 FK 없는 데이터 연계
fun handleUserCreatedEvent(event: UserCreatedEvent) {
val userId = event.userId
val userName = event.name
cache.save(userId, userName) // FK 없이 캐싱
}
📌 이점:
서비스 간 강한 결합 없이 데이터 공유 가능
데이터 동기화가 필요하면 이벤트 큐(Kafka, RabbitMQ) 사용 가능
❌ 하지만 FK 없이 설계할 때 고려할 점
데이터 정합성을 애플리케이션 레벨에서 직접 관리해야 함 → 데이터 무결성 체크를 API에서 해야 함.
데이터 삭제 시 고아 데이터(Orphan Data)가 남을 수 있음 → 삭제 정책을 별도로 설계해야 함.
JOIN 쿼리가 복잡해질 수 있음 → FK가 없으면 JOIN 대신 애플리케이션에서 여러 번 조회해야 할 수도 있음.
🚀 언제 FK 없이 설계할까?
FK 없는 설계가 유리한 경우 | FK가 필요한 경우 |
|---|---|
마이크로서비스 환경 (각 서비스가 개별 DB 사용) | 관계 무결성이 중요한 경우 (은행 시스템, ERP 등) |
빅데이터 처리 (ETL, 로그 데이터) | 데이터 정합성을 DB에서 보장해야 하는 경우 |
고성능 OLTP 시스템 (트랜잭션 처리량 중요) | JOIN을 많이 사용하는 OLAP 시스템 |
분산 데이터베이스 / Sharding 사용 | 데이터 모델이 고정적이고 잘 변하지 않는 경우 |
🔥 결론
FK 없이 설계하면 쓰기 성능이 향상되고, 스케일아웃(Sharding) 및 마이그레이션이 쉬워짐.
하지만 데이터 무결성을 보장하기 어렵고, 애플리케이션에서 관계를 직접 관리해야 하는 부담이 생김.
마이크로서비스 환경이나 대규모 트랜잭션 시스템에서 FK를 없애는 전략이 유리할 수 있음.
반대로 관계가 강하게 유지되어야 하는 시스템(예: 금융, ERP)은 FK가 있는 것이 안정적.
🔥 즉, FK를 없앨지 말지는 "성능 vs. 정합성" 트레이드오프를 고려해서 결정해야 합니다.!
——————
부록)
✅ CQRS 패턴에서 FK가 필요 없는 이유
CQRS(Command Query Responsibility Segregation)는 쓰기(Commands)와 읽기(Queries) 모델을 분리하는 아키텍처 패턴이야.
전통적인 RDBMS 설계에서는 쓰기와 읽기가 같은 테이블을 사용하고, 관계를 유지하려고 FK를 사용함.
CQRS에서는 쓰기 모델(Commands)과 읽기 모델(Queries)이 분리되므로 FK가 없어도 됨.
쓰기 모델: 비즈니스 로직을 처리하고, 데이터 정합성을 보장.
읽기 모델: 읽기 성능을 최적화한 형태로 데이터를 저장.
🔥 CQRS에서 FK 없이 데이터 관계를 관리하는 방식
1️⃣ 쓰기 모델 (Command Model)에서 ID 기반 저장
쓰기 모델은 정규화된 형태로 저장하지만, FK를 강제하지 않음.
대신, 참조할 데이터의 ID만 저장하고, 데이터 정합성은 애플리케이션 레벨에서 관리.
🔍 예제: 주문(Order)과 사용자(User)를 저장하는 Command Model (FK 없이 ID만 저장)
@Entity
class Order(
@Id @GeneratedValue
val id: Long = 0,
val userId: Long, // FK 없이 ID만 저장
val totalAmount: Double
)
userId는 실제 User 테이블을 참조하지 않음 → FK 없이도 데이터 저장 가능.데이터 정합성은 애플리케이션 레벨에서 검증 (예:
userId가 존재하는지 체크).
📌 이점:
✅ FK가 없으므로 서비스 간 독립성이 증가.
✅ 쓰기 성능 최적화 가능 (특히, 대량 삽입/업데이트 시 유리).
✅ 멀티 데이터베이스 지원 가능 (쓰기와 읽기 데이터베이스를 분리할 수 있음).
2️⃣ 읽기 모델 (Query Model)에서 최적화된 형태로 저장
읽기 모델은 조회 성능을 최적화한 형태로 저장하므로 FK가 필요 없음.
데이터를 비정규화(Denormalization) 해서 조인 없이 빠르게 조회 가능.
🔍 예제: 주문 내역을 조회하는 Query Model (FK 없이 직접 저장)
data class OrderDetail(
val orderId: Long,
val userName: String,
val totalAmount: Double
)
userName을 저장해두면 조인 없이 조회 가능.FK가 없기 때문에, 읽기 성능이 크게 향상됨.
📌 이점:
✅ 조인을 최소화하여 읽기 성능을 극대화.
✅ FK가 없어도 읽기 성능을 위해 비정규화된 데이터 저장 가능.
✅ 캐시나 NoSQL과 함께 사용하기 쉬움 (예: Redis, Elasticsearch).
3️⃣ 이벤트 기반 데이터 동기화 (Event Sourcing)
CQRS에서는 데이터를 동기화할 때 이벤트(Event Sourcing) 기반으로 관리할 수도 있음.
FK 없이도 이벤트 로그를 활용하여 데이터 정합성을 유지 가능.
🔍 예제: OrderCreatedEvent를 발행하여 읽기 모델을 갱신
data class OrderCreatedEvent(val orderId: Long, val userId: Long, val totalAmount: Double)
fun handleOrderCreated(event: OrderCreatedEvent) {
val user = userRepository.findById(event.userId)
val orderDetail = OrderDetail(event.orderId, user.name, event.totalAmount)
orderReadRepository.save(orderDetail) // 읽기 모델에 저장
}
쓰기 모델에서 Order가 생성되면 이벤트가 발행됨.
이벤트 리스너가 OrderDetail을 업데이트 → FK 없이도 데이터 정합성을 유지 가능.
📌 이점:
✅ 서비스 간 느슨한 결합(Loosely Coupled) 유지 가능.
✅ FK 없이도 데이터 정합성을 이벤트로 보장 가능.
✅ 비동기 처리가 가능하여 확장성이 뛰어남.