시작하며...
대부분의 ORM이 기본세팅으로 Entity간 연관관계가 설정될 경우 DDL 생성시 알아서 외래키 제약조건을 추가하는 것으로 알고 있다.
다만 자동 DDL 생성 옵션을 끄고 내가 직접 DDL을 작성할때는 잘 안썼다.
이렇게 외래키 제약조건이 무엇인지만 알고 있던 상황에서 우연히 외래키 제약조건을 사용 할지 말지 고민을 하고있는 글을 보게 되었고, 이 부분에 대해서 정확한 장단점을 알고 넘어가면 추후 명확한 근거와 함께 행동할 수 있을 듯 하여 글을 작성하기 시작했다.
외래키 제약조건을 사용하면 어떤 점들이 좋은지, 오버헤드는 얼마나 발생하는지, 만약 사용하지 않는다면 어떤 부분들을 신경 써 주어야 할지 알아보고자 한다.
외래키 제약조건
외래키 제약조건이란?
MySQL의 외래 키 제약조건은 데이터베이스 내 테이블 간의 관계를 정의하고, 참조 무결성을 보장하는 데 사용되는 조건이다.
이게 무슨소리인지 다시한번 톺아보자.
참조 무결성
RDB에서 참조 무결성이란, 테이블 간의 데이터 참조관계가 항상 유효한 상태를 유지하도록 보장하는 규칙이다.
쉽게 말해서 위와 같은 상황에 김성윤 사원은 화장실 청소팀임을 알 수 있지만, 박성윤씨는 무슨 부서에 속해있는지 알 수가 없다. RDB에서는 이러한 상황을 가만히 두고보지 않는다.
박성윤 사원의 부서 번호 22번으로 할당해 데이터를 삽입하려고 하거나, 김성윤 사원이 속한 화장실 청소 부서를 삭제하려고 하면 외래키 제약조건이 참조 무결성 위배 오류(Integrity constraint violation)를 발생시킨다.
외래키 제약 조건 사용방법
외래키 제약조건은 DDL(Data Definition Language)를 작성할 때, 즉 테이블을 생성할 때 추가가 가능하다. (물론 테이블 생성하고 나서 나중에 제약 조건만 추가할수도 있다.
-- 부서 테이블 생성
CREATE TABLE 부서 (
부서_번호 INT PRIMARY KEY, -- 부서 번호 (PK)
부서_이름 VARCHAR(100) NOT NULL -- 부서 이름
) ENGINE=InnoDB;
-- 사원 테이블 생성
CREATE TABLE 사원 (
사원_번호 INT PRIMARY KEY, -- 사원 번호 (PK)
사원_이름 VARCHAR(100) NOT NULL, -- 사원 이름
부서_번호 INT, -- 부서 번호 (FK)
FOREIGN KEY (부서_번호) REFERENCES 부서(부서_번호)
ON DELETE SET NULL -- 부서가 삭제되면 사원의 부서 번호는 NULL로 설정
ON UPDATE CASCADE -- 부서 번호가 변경되면 사원의 부서 번호도 함께 변경
) ENGINE=InnoDB;
위 방식으로 부서를 먼저 만들고, 사원 테이블에 부서 테이블을 참조 한다는 제약조건을 달아 주어 설정할 수 있다. 이렇게 해두면, 사원이나 부서를 생성 및 삭제할 때 참조 무결성을 DBMS가 보장해주고, 명시적으로 사원과 부서는 연관관계를 가지는 테이블임을 알려줄 수 있다.
외래키 제약조건 필수일까?
그거 무조건 써야되는거 아니었음?
정답은 "아니오"다.
잘 생각해보면, 연관관계를 설정하는 것과 외래키 제약조건을 설정하는 것은 사실 아무런 관계가 없다.
-- 부서 테이블 생성
CREATE TABLE 부서 (
부서_번호 INT PRIMARY KEY, -- 부서 번호 (PK)
부서_이름 VARCHAR(100) NOT NULL -- 부서 이름
) ENGINE=InnoDB;
-- 사원 테이블 생성
CREATE TABLE 사원 (
사원_번호 INT PRIMARY KEY, -- 사원 번호 (PK)
사원_이름 VARCHAR(100) NOT NULL, -- 사원 이름
부서_번호 INT -- 부서 번호 (FK 역할 가능성 있음)
) ENGINE=InnoDB;
사실 그냥 이렇게 테이블 구성하고 사용해도 아무런 문제가 없다.
사원을 통해서 부서 정보를 가져와야 할 때도, 그냥 JOIN을 수행하면 정상적으로 동작한다.
데이터 무결성(참조 무결성)이 위배된 경우, 사원 릴레이션과 부서 릴레이션에 JOIN을 수행하면 부서는 NULL로 나온다.
그래서 뭐 어쩌라고?
외래키 제약조건을 의도적으로 사용하지 않는 선택에는 몇 가지 이점이 있을 수 있지만, 동시에 개발 및 운영 시 추가적으로 신경 써야 하는 부분들 역시 생긴다.
특징
- 성능 향상(특히 대용량 데이터 처리 시) [장점]
외래키 제약조건이 없을 시 데이터 삽입(insert), 갱신(update), 삭제(delete) DBMS단의 참조 무결성이 필요 없기에, 이러한 작업에 소요되는 오버헤드가 줄어든다. (사원을 생성하는 경우, 해당 사원의 부서가 실제로 존재하는지 확인 할 필요가 없다.)
외래키 제약조건을 유지한채로 부서 데이터 100개, 사원 데이터 1,000,000개를 삽입 해보았다.
위에서 언급한 부서, 사원 테이블의 경우 외래키 제약조건의 유무로 100만 건의 데이터 삽입 시 25.8%의 성능 차이가 났다. 매우 간단한 수준의 테이블이라서 그렇지, 외래키가 많은 데이터라면 훨씬 더 많은 오버헤드가 발생 할 것이다.
- 유연한 데이터 로딩 순서 (병렬 삽입에 용이) [장점]
외래키 제약조건을 사용하면 데이터 삽입 간 테이블 간의 관계를 확인하고, 데이터 우선순위를 고려해야 한다.
예를 들어 사원 데이터를 생성하기 위해서는 부서 데이터가 반드시 존재해야한다. 외래키 제약조건 사용 시 순서를 맞추어 삽입하지 않으면 오류가 발생 할 것이다.
이러한 제약 상황은 한꺼번에 많은 수의 데이터를 병렬 처리 (배치 연산) 해야 할 때 발목을 잡는다. 애플리케이션 단에서 이러한 우선순위를 고려하지 않고 자유롭게 로직을 작성하여 비교적 효율성 있는 코드를 작성할 수 있다.
- 데이터 무결성 유지 [단점]
외래키 제약조건이 없으면, 예를 들어 부모 테이블의 데이터를 삭제했을 때 자식 테이블에 고아(Orphan) 레코드가 남게 될 위험이 있다. 또한, 새로운 데이터들이 서로 연관되어야 하는 상황에서, 개발자의 실수로 데이터 무결성이 훼손될 가능성역시 무시할 수 없다.
이를 위해서 애플리케이션 레벨에서 데이터 무결성을 보존하기 위한 장치들을 추가하고, 주기적으로 무결성을 검증하는 로직을 작성한다.
하지만 결국 이러한 데이터 무결성방지를 오로지 애플리케이션 레벨로 위임하는 것은 비교적 위험하다.
외래키 제약조건과 인덱스
MySQL에서 외래키 제약조건은 설정 시 자동으로 FK 속성에 인덱스가 부여된다.
이렇게 부여한 인덱스는 추후 JOIN시 조회 성능에 영향을 준다.
예를 들어 다음과 같은 쿼리를 실행한다고 했을 때
SELECT e.*, d.부서_이름
FROM 사원 e
INNER JOIN 부서 d
ON e.부서_번호 = d.부서_번호;
외래키 제약조건이 있을 때와 없을 때의 쿼리플랜은 다음과 같다.
외래키 제약 조건이 존재하는 경우, 풀스캔을 비교적 행 수가 적은 부서 쪽(100개)에서 진행하고, 데이터가 많은 사원쪽에서 ref 를 통해 접근하여 동작 하고 있다. (데이터 개수는 두 환경 모두 1,000,000개로 동일 하다. rows는 옵티마이저가 추정한 값이라 좀 이상하게 나오는 듯 하다.)
반대로 외래키 제약조건이 존재하지 않는 경우, 데이터가 많은 사원 쪽에서 풀스캔 (ALL)을 진행하고, 부서 쪽에서 eq_ref를 통해 접근하여 비교적 비효율 적이다.
이렇게 외래키 제약조건을 사용하지 않은 상태로는 조인 시 꽤나 비효율 적으로 동작하기에, 외래키 제약조건을 사용하지 않은 경우 반드시 FK에 별도로 인덱스를 걸어 주자.
결론
외래키 제약조건을 사용하지 않기 위해서는 어떠한 부작용이 발생하는지 잘 확인하고, 그에 해당하는 대안책을 잘 마련해 두어야 할 것이다.