우리 백엔드 팀에서 최근 논의중인 주제
시니어 멘토님께서 던져주신 화두로 모두가 머리싸매고 고민하고 있는 모습이 아주 보기좋다.
나 역시도 잘 몰랐던 내용이라 정리하고 넘어간다
DB에 물리적 FK 걸지마라
외래키는 테이블 간의 관계를 설정하고, 참조 무결성(데이터의 일관성)을 보장하는 중요한 역할을 한다.
유효성 체크 혹은 CASCADE 처리를 DB레벨 에서 진행하면서 애플리케이션 레벨에서 복잡한 로직을 생략할 수 있다는 장점도 있다.
[단점 : 대규모시스템, 개발편의성, 성능, DB수정]
1. 대규모시스템
다만 분산환경에 가면 수평 확장이나 샤딩을 하면서 FK 제약을 유지하는 것이 곤란할 수가 있다. 또 데이터가 복잡해지다보면 차라리 외래키 없이 애플리케이션 레벨에서 데이터 무결성 관리하는게 나을 수도 있다.
2. 개발 편의성
외래키를 물리적으로 걸다보면 데이터가 삭제되거나 업데이트될 때, 여러 관련된 데이터들이 함께 변경되거나 삭제되어야 하는 경우가 발생할 수 있습니다. 이로 인해 시스템이 비효율적이거나 복잡해질 수 있다.
예를들어 회원이 탈퇴하면 회원과 관련된 모든데이터 (경험치, 채팅로그 등)을 삭제하거나 변경해야하는데 이 때 연관된 모든 테이블에서 해당 회원 정보를 찾아 하나하나 수동으로 처리해야한다면 관리가 어려울 수 있다.
또 테스트 환경에서도 FK가 설정된 경우 데이터를 삭제하거나 부분적으로 테스트할 때 참조무결성 오류걸리면서 번거러울 수 있다.
외래키를 걸지 않는다면 애플리케이션 로직을 통해 데이터를 유연하게 자유롭게 다룰 수 있을 것
(이 때 DB레벨에는 Foreign key 제약을 걸지 않고 테이블에 연관된 ID 값만 저장하여 관계를 보장하는 것을 외래키를 논리적으로 설정하고 물리적으로 설정하지 않는다고 한다)
3. 성능문제
또 외래키가 설정되어 있으면 데이터가 삽입되거나 갱신될 때마다 외래키 제약 검사를 해야해서 성능에 영향을 끼칠 수 있다 (예를 들어, 새로운 데이터가 추가되면 다른 테이블에 해당 데이터가 존재하는지 체크하는 과정에서 시간이 더 소요)
4. DB 수정
좀 더 자세히 얘기하면, ALTER 로 테이블을 수정하는 상황을 생각해보자.
ALTER TABLE users MODIFY id BIGINT;
이렇게 id 컬럽을 변경하려면 이 id 를 참조하는 모든 테이블의 user_id 의 key 가 걸려있을 것이므로 다 변경해주어야함.
외래키가 설정된 칼럼의 크기나 데이터타입을 변경하고자 하려면 외래키를 임시로 제거 한 후 변경을 가하고 다시 외래키를 걸어줘야한다.
방법1. Soft Delete (물리적 FK 걸려있을 경우)
UPDATE users SET is_deleted = true WHERE id = 123;
회원테이블에 is_deleted 같은 칼럼을 추가하여 탈퇴상태를 관리하면 실제 연관된 테이블의 데이터를 삭제하지 않고 상태만 변경한다. 후에 경험치나 채팅로그 등에서는 회원이 삭제된 상태인지만 확인하고 이를 필터링하여 처리
방법 2. Cascade Delete/Update (물리적 FK 걸려있을 경우)
CREATE TABLE experience (
id INT PRIMARY KEY,
user_id INT,
experience_points INT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
외래키를 Cascade 처리하면 회원이 탈퇴할때 연관된 경험치도 함께 삭제되도록 설정할 수 있다.
방법 3. 중간테이블 활용 (물리적 FK 없애고 논리적 FK만 설정)
-- 회원 테이블 (Member)
CREATE TABLE members (
id UUID PRIMARY KEY,
nickname VARCHAR(50) NOT NULL
);
-- 경험치 테이블 (Experience)
CREATE TABLE experiences (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- FK 없이 논리적인 관계만 유지
experience_points INT DEFAULT 0
);
experiences 의 user_id 에 FK를 직접 걸지 않고 애플리케이션 레벨에서 무결성 보장.
삭제할 경우엔 user_id 를 조회하여 삭제
조회가 빈번하면 인덱싱걸어라
테이블에서 특정 컬럼을 자주조회하거나 필터링하는 경우 인덱스를 추가하면 조회속도를 크게 개선할 수 있다.
CREATE TABLE chat_logs (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- FK 없이 논리적으로 관계만 설정
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
이 테이블에서 다음과 같이 조회쿼리를 자주 사용한다고 가정하자
SELECT * FROM chat_logs WHERE user_id = '550e8400-e29b-41d4-a716-446655440000';
user_id 컬럼에 인덱스가 없다면 채팅로그의 테이블의 모든 행을 하나씩 검색하면서 userI-id 와 일치하는 데이터를 찾아야한다 O(N)이겠지
CREATE INDEX idx_chat_logs_user_id ON chat_logs(user_id);
이 때 user_id 에 인덱스를 건다면, 인덱스는 B-Tree나 Hash 구조로 저장되어있어서 매우 빠른 조회를 할 수 있다 O(1)이겠지
특히 JOIN, WHERE, ORDER BY 등에 자주활용되는 칼럼이면 인덱싱은 필수이다.
다만 한 테이블에 인덱스가 너무 많다면 INSERT 시 모든 행에 대한 인덱스도 추가 해야하므로 성능상 트레이드오프가 있을 수 있다. 꼭 한테이블이 아니더라도 연관관계가 있으면 발생가능성이 있으니 잘 하자?
'[개발일지] > CS 스터디 플랫폼' 카테고리의 다른 글
16장. 이미지 어디서 어떻게 관리할까 S3? 파이어베이스? (2) | 2025.04.11 |
---|---|
15장. 요청 본문 로깅 어디서 하지? 또 어떤 로그를 찍어놓을까 Filter Interceptor AOP (1) | 2025.03.29 |
13장. MySQL에서 되던게 PostgreSQL 에선 왜 안돼? (JPA의 @GeneratedValue 전략 차이) (1) | 2025.03.25 |
12장. IntelliJ가 해주는 역할이 뭐였을까. Cursor AI로 넘어오면서 생긴 일들 (=Cursor 쓰면 새로운 디펜던시 반영 어떻게 (0) | 2025.03.20 |
11장. 상태관리 라이브러리 Context API 말고 다른거 쓰자. Redux? Zustand? (0) | 2025.02.28 |