복합 키란?
복합 키란 SQL의 null이 아니고 테이블에서 하나의 컬럼을 지정하여 기본 키(PK, Primary Key)로 사용하는 대신, 테이블의 특정 행을 식별하기 위해서 둘 이상의 컬럼을 조합하여 PK로 사용하는 방법이다.
복합 키는 컬럼들이 결합되어 사용되는만큼, 행의 고유성은 보장되지만(같은 복합 키를 가지는 행은 없지만) 복합 키의 개별 컬럼에 대한 고유성은 보장되지 않는다.
복합 키는 둘 이상의 컬럼을 조합하여 만들 수 있고, 기본 키와 마찬가지로 null 값을 가질 수 없다.
복합 키를 도입한 이유
프로젝트를 구성하던 중 Member와 Category, Board와 Category는 각자 서로 ManyToMany 관계를 가지는 구조로 작성해야 했다.
ManyToMany 관계를 사용하지 않기 위해서 중간 테이블을 두고 OneToMany - ManyToOne 관계로 묶게 되었는데, 그러다 보니 아래와 같이 컬럼이 딱 2개인 테이블이 존재하게 되었다.
해당 테이블들에서는 테이블의 PK를 지정하는 대신, 두 컬럼을 복합 키로 지정하게 될 경우 PK 없이 두 컬럼을 고유하게 식별할 수 있다는 점에 도입하게 되었다.
복합 키 적용하기
복합 키를 적용하는 방법은 두 가지가 있다. 두 가지 방법 모두 별도의 복합 키 클래스를 만들어야하고, 해당 클래스를 Id로 사용하여 기본 키로 등록한다.
위의 테이블에서 컬럼명을 Consumer-Provider 개념으로 변경하여, 다음과 같이 소비하는 주체인 memberId와 boardId를 ConsumerId로 categoryId를 ProviderId로 변경했다.
그에 맞춰 복합 키로 사용할 외부의 클래스를 선언해주었다.
복합 키 클래스
1.
@IdClass
복합 키를 도입하는 첫 번째 방법은 @IdClass 어노테이션을 사용하여, 아래와 같이 외부 복합 키 클래스를 Id로 사용하도록 등록하고 각 Id를 따로 선언하는 방법이다.
위처럼 해당 클래스 안에서 어떤 컬럼들이 복합 키의 Id로 사용되는지 직접 볼 수 있다.
하지만 Id로 선언되는 컬럼이 복합 키 클래스의 멤버 변수와 이름이 다르면 런타임 에러가 발생하고, 복합 키로 사용하는 컬럼마다 @Id 식별자를 붙여주어야 해서 복합 키 컬럼이 많아질수록 코드가 길어진다는 단점이 있다.
2.
@EmbeddedId
두 번째 방법은 @EmbeddedId 어노테이션을 사용해, 아래와 같이 복합 키 클래스 자체를 Id로 들고오는 방법이다.
사용하기 편하고 간결하며 오타에 따른 오류가 발생할 일이 없다.
하지만 복합 키 내의 변수들을 인지하고 있어야 하고 모른다면 해당 클래스에 타고 들어가 직접 확인해야한다. 또한 @EmbeddedId 방식을 사용하면 JPQL에서 객체 탐색을 더 많이 하게 된다.
복합 키의 단점
1.
FK를 맺을 때 사이드 이펙트가 크다.
CREATE TABLE library (
region_no varchar(10),
library_name varchar(50),
CONSTRAINT PK_library PRIMARY KEY (region_no, library_name)
);
CREATE TABLE book (
book_id int,
author varchar(50),
name varchar(100),
CONSTRAINT PK_book PRIMARY KEY (book_id)
)
SQL
복사
위와 같이 region_no와 library_name을 복합 키로 가지는 library 테이블과 book_id를 PK로 가지는 book 테이블이 있을 때, 책이 어느 도서관에 있는지 알기 위해 book 테이블에 도서관 식별자로 FK 관계를 구성하려면 region_no와 library_name 모두 포함해야하는 불편함이 있다.
2.
복합 키를 설계할 때 어떤 쿼리가 자주 호출되는지 분석하여 고려를 해야한다.
복합 키를 사용하고 복합 인덱스를 적용한다면 단일 인덱스에 비해 조회 속도가 개선될 수 있지만, 삽입/수정/삭제에서는 성능이 감소하게 된다.
또한 이후의 3번 단점과도 이어지는데, 컬럼의 순서에 따라 인덱스의 성능 차이가 크게 발생할 수 있다.
3.
PK로 지정한 컬럼은 자동으로 인덱스가 생성되는데, 복합 키를 통해 조회하면 인덱스 스캔이 제대로 수행되지 않을 수 있다.
구체적으로는 a, b, c 컬럼에 대해 복합 키를 지정했다면, 카디널리티를 높이기 위해 인덱스를 a, b, c 모두를 포함하도록 구성하게 될 것이다.
•
where a = 1 and b = 1 : 인덱스 스캔
•
where b = 1 and c = 1 : 풀 스캔
•
where a = 1 and c = 1 : 인덱스 스캔
이처럼 선구 조건(컬럼)이 나와야 일부라도 인덱스 스캔이 수행된다. B-Tree로 구성되어 있는 인덱스가 선구 조건이 존재해야 검색을 수행할 수 있기 때문이다.
이는 사전에서 ‘가’로 시작하는 단어들을 찾기는 쉽지만, 중간에 ‘가’를 포함하는 단어들을 찾으려면 사전 전체를 훑어봐야하는 것과 동일하다.
이러한 이유로 복합 키에는 인덱스를 적용할 때 많은 것을 고려해야하며, 인덱스를 구성할 때도 가급적 카디널리티가 높은 것부터 낮은 순으로 구성하는게 좋다.
4.
제약 조건을 변경 시 PK 전체 수정이 필요하다.
애플리케이션 구조 변경으로 인해 제약 조건이 변경된다면, PK 전체를 수정해야 할 수도 있다. 또한 다른 테이블과 FK를 맺고 있다면, 해당 테이블의 내용도 전부 수정해줄 필요가 있다.
5.
equals(), hashCode() 메서드를 재정의해주어야한다.
복합 키를 비교할 때 equals() 메서드와 hashCode() 메서드를 재정의(Override) 해주는 것이 좋다.
PushLog log1 = new PushLog(1L, 2L);
PushLog log2 = new PushLog(1L, 2L);
// 메모리 주소가 다르기 때문에 hashCode에서 false 반환
cls1.equals(cls2);
Java
복사
6.
너무 많은 컬럼을 복합 키로 포함하면 성능이 저하 된다.
복합 키의 모든 컬럼이 인덱스에 포함되어 기본적으로 저장 공간이 증가되는데, 컬럼이 더 많아진다면 인덱스를 통한 조회나 인덱스 재구성 시에 성능이 저하될 수 있다.
7.
코드 작성의 번거로움
복합 키가 주로 사용되는 상황
이런 이유로 복합키는 데이터를 식별하는데 크게 의미가 없는 경우에 사용되고, 하나의 ID로 데이터를 조작하는 일이 없고 복합키를 통한 조회가 자주 발생한다면 인덱스를 활용 가능하기 때문에 사용된다.
복합 키가 주로 사용되는 상황은 다음과 같다.
1.
단일 컬럼으로 유니크 값을 보장할 수 없는 경우
사실 위 사유는 추가적인 유니크한 Id 컬럼을 두면 해결되는 문제이기 때문에 주된 사용 이유가 아니다. 다만 추가적인 컬럼 없이 유니크 제약 조건을 생성할 수 있다는 점에서 저장되는 데이터를 조금 아낄 수 있다.
2.
현실 세계의 복잡한 비즈니스 규칙을 데이터베이스에 표현하기 위해
CREATE TABLE reservations (
room_number INT,
check_in_date INT,
guest_id INT,
PRIMARY KEY(room_number, check_in_date)
);
SQL
복사
위의 예시와 같이 호텔 예약 시 같은 방은 동일한 날짜에 중복해서 예약이 불가능함을 표현하기 위해 사용될 수 있다.
이와 같이 비즈니스적 규칙을 데이터베이스에 적용하는 경우에는 자연 키(Natural Key)의 의미를 더 살릴 수 있고, 그로 인해 데이터의 자체 문서화 효과를 얻을 수 있다. 또한 실수로 중복 데이터가 들어가는 것을 원천 차단하여, 데이터 정합성 보장을 더 확실하게 지켜낼 수 있다.
3.
레거시 시스템과의 호환성
기존 시스템이나 외부 시스템과의 연동 시에 복합 키로 구성되어 있다면, 복합 키를 사용하여 시스템 구축 비용이나 데이터 마이그레이션 비용 절감할 수 있다.
4.
분산 시스템 활용
복합 키 구성을 샤딩 전략이나 파티셔닝 전략과 잘 결합하면, 분산 저장에 용이하게 시스템을 구축할 수 있다. 예를 들면 지역을 나타내는 region_id와 시간을 나타내는 timestamp를 복합 키로 구성하면, 지역별/시간별로 데이터를 분산하여 저장하는 것이 용이하다.
하지만 현대적인 애플리케이션에서는 별개의 컬럼을 PK로 두는 surrogate key(대체 키, 주로 auto-increment Id 혹은 UUID를 사용)를 더 많이 사용한다.
•
간단한 PK 관리
•
외래 키 참조 부작용 최소화
•
ORM 사용 용이
•
키 값 변경 시 영향 범위 최소화
•
유연한 인덱스 관리
위와 같은 이유로 인해 복합 키는 잘 사용되지 않고, 추가적인 비즈니스 규칙을 살리고 싶다면 복합 유니크 인덱스를 추가하는 것을 선호한다. 다시 말해 surrogate key + 복합 유니크 인덱스 조합을 통해 비즈니스 규칙을 살려 저장되는 데이터를 관리한다.
결론
사실 복합 키를 처음 사용할 때는, 두 컬럼을 합쳐서 유니크 조건을 걸 수 있고 그를 기본 키로 사용한다는 개념에 대해서만 이해한 채로 사용했다. 복합 키가 장점이나 단점에 대해 전혀 모른채로 사용했었다.
사실 이런 내용들을 고려해보면 인덱스를 사용하지 않는 내 프로젝트에서는 복합 키를 사용할 이유가 전혀 없다. 복합 키를 왜 썼어요라는 질문을 받은 후에야 이를 고민하게 된 과거의 나를 반성하자.