fontcolor_theme
Deepkit ORM

복합 Primary Key

Composite Primary Key란, 하나의 엔티티가 여러 개의 Primary Key를 가지며 이들이 자동으로 결합되어 "composite primary key"를 이루는 것을 의미합니다. 이러한 방식의 데이터베이스 모델링에는 장단점이 있습니다. 우리는 Composite Primary Key가 그 장점에 비해 실무적인 단점이 매우 크다고 판단하며, 따라서 bad practice로 간주되어 피해야 한다고 봅니다. Deepkit ORM은 Composite Primary Key를 지원하지 않습니다. 이 장에서는 그 이유를 설명하고 (더 나은) 대안을 제시합니다.

단점

조인은 단순하지 않습니다. RDBMS에서 매우 최적화되어 있더라도, 애플리케이션에서는 쉽게 통제 불가능한 상수 복잡도를 만들어 성능 문제로 이어질 수 있습니다. 성능은 쿼리 실행 시간뿐 아니라 개발 시간 측면에서도 해당됩니다.

조인

각 개별 조인은 관여하는 필드가 많아질수록 더 복잡해집니다. 많은 데이터베이스가 여러 필드로 조인하더라도 본질적으로 느려지지 않도록 최적화를 구현했지만, 개발자는 이러한 조인을 항상 세심하게 검토해야 합니다. 예를 들어 키를 하나라도 빠뜨리면 미묘한 오류로 이어질 수 있습니다(모든 키를 명시하지 않아도 조인이 동작하기 때문). 따라서 개발자는 전체 Composite Primary Key 구조를 알고 있어야 합니다.

인덱스

여러 필드로 구성된 인덱스(즉, Composite Primary Key)는 쿼리에서의 필드 순서 문제를 겪습니다. 데이터베이스 시스템이 특정 쿼리를 최적화할 수는 있지만, 구조가 복잡해질수록 모든 정의된 인덱스를 올바르게 사용하는 효율적인 연산을 작성하기가 어려워집니다. 여러 필드가 있는 인덱스(예: Composite Primary Key)의 경우, 데이터베이스가 실제로 인덱스를 사용하도록 필드를 올바른 순서로 정의하는 것이 일반적으로 필요합니다. 순서가 올바르게 지정되지 않으면(예: WHERE 절에서) 데이터베이스가 인덱스를 전혀 사용하지 않고 전체 테이블 스캔을 수행할 수 있습니다. 어떤 데이터베이스 쿼리가 어떤 방식으로 최적화되는지 아는 것은 고급 지식이며, 신규 개발자에게는 보통 없는 지식입니다. 그러나 Composite Primary Key를 사용하기 시작하면 데이터베이스를 최대한 활용하고 리소스를 낭비하지 않기 위해 이러한 지식이 필요해집니다.

마이그레이션

특정 엔티티를 고유하게 식별하기 위해 추가 필드가 필요하다고 결정하는 순간(즉, Composite Primary Key가 됨), 해당 엔티티와 관계를 가진 데이터베이스의 모든 엔티티를 조정해야 합니다.

예를 들어, user 엔티티가 Composite Primary Key를 가지고 있고, 여러 테이블(예: 피벗 테이블 audit_log, groups, posts)에서 이 user에 대한 외래 키를 사용한다고 가정해 봅시다. user의 Primary Key를 변경하는 순간, 이러한 모든 테이블도 마이그레이션에서 함께 조정되어야 합니다.

이는 마이그레이션 파일을 훨씬 더 복잡하게 만들 뿐만 아니라, 마이그레이션 실행 시 큰 다운타임을 초래할 수 있습니다. 스키마 변경은 보통 전체 데이터베이스 락 또는 적어도 테이블 락을 필요로 하기 때문입니다. 인덱스 변경과 같은 큰 변경으로 영향을 받는 테이블이 많을수록 마이그레이션 시간은 길어집니다. 그리고 테이블이 클수록 마이그레이션 시간은 더 오래 걸립니다. audit_log 테이블을 생각해 보세요. 이러한 테이블은 보통 매우 많은 레코드(수백만 건 등)를 가지고 있으며, 오직 Composite Primary Key를 사용하기로 결정했고 user의 Primary Key에 필드를 하나 더 추가했다는 이유만으로 스키마 변경 중에 이들을 모두 건드려야 합니다. 이러한 모든 테이블의 크기에 따라, 이는 마이그레이션 변경을 불필요하게 더 비싸게 만들거나, 경우에 따라 User의 Primary Key 변경이 더 이상 재정적으로 정당화될 수 없을 만큼 비싸게 만들 수도 있습니다. 이는 보통(예: user 테이블에 unique 인덱스를 추가하는 등의) 우회책으로 이어지며, 기술 부채를 낳고 머지않아 레거시 목록에 오르게 됩니다.

대규모 프로젝트에서는 이는 큰 다운타임(수 분에서 수 시간)에 이를 수 있으며, 심지어 본질적으로 테이블을 복사하고, 레코드를 고스트 테이블에 삽입한 뒤, 마이그레이션 후 테이블을 앞뒤로 이동시키는 전혀 새로운 마이그레이션 추상화 시스템의 도입으로 이어지기도 합니다. 이러한 추가 복잡성은 Composite Primary Key를 가진 다른 엔티티와 관계를 가진 모든 엔티티에 강제로 부과되며, 데이터베이스 구조가 커질수록 더 커집니다. 이 문제는(Composite Primary Key를 완전히 제거하는 것 외에는) 해결 방법이 없어 점점 악화됩니다.

찾기 용이성

데이터베이스 관리자나 Data Engineer/Scientist라면 보통 데이터베이스에서 직접 작업하고, 필요할 때 데이터를 탐색합니다. Composite Primary Key를 사용하면, SQL을 직접 작성하는 모든 사용자는 관련된 모든 테이블의 올바른 Primary Key(그리고 올바른 인덱스 최적화를 위한 컬럼 순서) 를 알아야 합니다. 이러한 오버헤드는 데이터 탐색, 리포트 생성 등을 복잡하게 만들 뿐 아니라, Composite Primary Key가 갑자기 변경되면 오래된 SQL에서 오류를 유발할 수 있습니다. 오래된 SQL은 아마도 여전히 유효하고 잘 실행되겠지만, 조인에서 Composite Primary Key의 새 필드가 누락되어 갑자기 잘못된 결과를 반환합니다. 여기서는 Primary Key를 하나만 두는 것이 훨씬 쉽습니다. 이렇게 하면 데이터를 찾기 쉬워지고, 예를 들어 user 객체를 고유하게 식별하는 방식을 변경하기로 하더라도 오래된 SQL 쿼리가 여전히 올바르게 동작하도록 보장할 수 있습니다.

리팩토링

하나의 엔티티에서 Composite Primary Key를 사용하게 되면, 키를 리팩토링할 때 상당한 추가 리팩토링이 필요할 수 있습니다. Composite Primary Key를 가진 엔티티는 일반적으로 단일 고유 필드가 없으므로, 모든 필터와 링크에는 Composite Key의 모든 값이 포함되어야 합니다. 이는 보통 코드가 Composite Primary Key를 알고 있다는 가정에 의존하게 된다는 뜻이며, 따라서 모든 필드를 가져와야 합니다(예: user:key1:key2 같은 URL). 일단 이 키가 변경되면, URL, 커스텀 SQL 쿼리 등 이 지식이 명시적으로 사용되는 모든 곳을 다시 작성해야 합니다.

ORM은 일반적으로 값을 수동으로 지정하지 않아도 조인을 자동으로 생성하지만, URL 구조나 커스텀 SQL 쿼리 같은 다른 모든 사용 사례에 대한 리팩토링을 자동으로 처리할 수 없으며, 특히 리포팅 시스템 및 모든 외부 시스템 등 ORM을 전혀 사용하지 않는 곳에서는 더욱 그렇습니다.

ORM 복잡성

Composite Primary Key를 지원하면 Deepkit ORM과 같은 강력한 ORM의 코드 복잡성이 엄청나게 증가합니다. 코드와 유지보수가 더 복잡해져 비용이 증가할 뿐 아니라, 사용자로부터 발생하는 엣지 케이스가 늘어나 이를 수정·유지해야 합니다. 쿼리 레이어, 변경 감지, 마이그레이션 시스템, 내부 관계 추적 등 전반의 복잡성이 크게 증가합니다. Composite Primary Key를 지원하는 ORM을 구축하고 지원하는 데 수반되는 전체 비용은 전반적으로 너무 높아 정당화될 수 없기에, Deepkit은 이를 지원하지 않습니다.

장점

이와 별개로, Composite Primary Key에도 장점은 있습니다. 다만 매우 피상적입니다. 각 테이블에 가능한 적은 인덱스를 사용하면, 유지해야 할 인덱스가 적어 쓰기(삽입/수정)가 더 효율적입니다. 또한 모델의 구조가 약간 더 깔끔해지기도 합니다(보통 컬럼이 하나 적어지므로). 그러나 요즘에는 순차적으로 정렬된 자동 증가 Primary Key와 비증가형 Primary Key 간의 차이는 사실상 무시할 수 있습니다. 디스크 공간은 저렴하고, 해당 작업은 보통 "append-only" 작업으로 매우 빠르기 때문입니다.

물론 소수의 엣지 케이스(그리고 매우 특정한 일부 데이터베이스 시스템)에서는 처음에는 Composite Primary Key로 작업하는 것이 더 나을 수 있습니다. 하지만 그러한 시스템에서도(모든 비용을 고려하면) 이를 사용하지 않고 다른 전략으로 전환하는 편이 전반적으로 더 타당할 수 있습니다.

대안

Composite Primary Key의 대안은 단일 자동 증가 숫자 Primary Key(보통 "id")를 사용하고, Composite Primary Key는 여러 필드로 구성된 unique 인덱스로 옮기는 것입니다. 사용되는 Primary Key(예상 행 수에 따라 다름)에 따라 "id"는 레코드당 4바이트 또는 8바이트를 사용합니다.

이 전략을 사용하면, 더 이상 위에서 설명한 문제들을 강제로 고민하고 해결책을 찾아야 하지 않으므로, 끊임없이 성장하는 프로젝트의 비용을 크게 줄일 수 있습니다.

이 전략은 구체적으로 각 엔티티가 보통 가장 앞에 "id" 필드를 하나 가지고, 기본적으로 이 필드를 고유 행 식별 및 조인에 사용한다는 뜻입니다.

class User {
    id: number & PrimaryKey & AutoIncrement = 0;

    constructor(public username: string) {}
}

Composite Primary Key의 대안으로, 대신 여러 필드로 구성된 unique 인덱스를 사용합니다.

@entity.index(['tenancyId', 'username'], {unique: true})
class User {
    id: number & PrimaryKey & AutoIncrement = 0;

    constructor(
        public tenancyId: number,
        public username: string,
    ) {}
}

Deepkit ORM은 MongoDB를 포함해 자동 증가 Primary Key를 자동으로 지원합니다. 이는 데이터베이스에서 레코드를 식별하는 데 선호되는 방법입니다. 다만, MongoDB의 경우 간단한 Primary Key로 ObjectId(_id: MongoId & PrimaryKey = '')를 사용할 수 있습니다. 숫자형 자동 증가 Primary Key의 대안으로 UUID도 동일하게 사용할 수 있습니다(단, 인덱싱 비용이 더 비싸므로 약간 다른 성능 특성이 있습니다).

요약

Composite Primary Key는 본질적으로 한 번 도입되면 향후 모든 변경과 실무적 사용의 비용이 훨씬 높아진다는 것을 의미합니다. 처음에는(컬럼이 하나 적으므로) 깔끔한 아키텍처처럼 보일 수 있지만, 프로젝트가 실제로 개발되기 시작하면 실무적인 비용이 크게 증가하며, 프로젝트가 커질수록 그 비용은 계속 상승합니다.

장점과 단점의 비대칭성을 고려하면, 대부분의 경우 Composite Primary Key는 정당화될 수 없습니다. 비용이 이점보다 훨씬 큽니다. 사용자뿐 아니라 ORM 코드를 작성·유지하는 우리에게도 마찬가지입니다. 이러한 이유로 Deepkit ORM은 Composite Primary Key를 지원하지 않습니다.

English中文 (Chinese)한국어 (Korean)日本語 (Japanese)Deutsch (German)