현대 웹 애플리케이션을 위한 기본 키(Primary Key) 논쟁
Lukas Schneider
DevOps Engineer · Leapcell

소개
끊임없이 진화하는 현대 웹 애플리케이션 개발 환경에서 아키텍트와 개발자들 사이에서 종종 상당한 논쟁을 불러일으키는 근본적인 결정이 있습니다. 바로 기본 키 전략의 선택입니다. 보편적으로 고유한 식별자(UUID), 큰 정수(BIGINT) 또는 이메일 주소와 같은 의미 있는 자연 키를 사용할지 여부에 대한 이 겉보기에는 간단한 결정은 애플리케이션의 확장성, 성능, 데이터 무결성 및 심지어 팀 작업 흐름에 지대한 영향을 미칠 수 있습니다. 애플리케이션이 복잡해지고, 분산 시스템에 걸쳐 확장되며, 강력한 데이터 모델을 요구함에 따라 각 접근 방식의 미묘한 차이를 이해하는 것이 중요해집니다. 이 글은 이러한 논의의 핵심을 파고들어 UUID, BIGINT 및 자연 키의 장단점을 분석하여 개발자가 최신 웹 프로젝트를 위해 정보에 입각한 결정을 내릴 수 있도록 안내합니다.
핵심 용어
비교 분석에 들어가기 전에 이 논의의 중심이 되는 핵심 용어에 대한 명확한 이해를 확립해 봅시다.
- 기본 키(PK): 데이터베이스 테이블에서 각 행을 고유하게 식별하는 열 또는 열 집합입니다. 기본 키는 개체 무결성을 강제하고 테이블 간의 관계를 설정하는 데 중요합니다.
 - UUID(Universally Unique Identifier): 컴퓨터 시스템에서 정보를 고유하게 식별하는 데 사용되는 128비트 숫자입니다. UUID는 중앙 기관 없이 생성되므로 충돌 가능성이 매우 낮습니다. 일반적으로 
a1b2c3d4-e5f6-7890-1234-567890abcdef와 같은 36자 길이의 16진수 문자열로 표현됩니다. - BIGINT: 큰 정수, 일반적으로 64비트를 나타내는 데이터 유형입니다. 기본 키의 맥락에서 BIGINT는 종종 자동 증가(auto-incrementing)되어 데이터베이스가 각 새 레코드에 자동으로 순차적이고 고유한 번호를 할당합니다.
 - 자연 키(Natural Key): 엔티티의 본질적인 부분이며 고유하게 설명하는 기존 속성을 하나 이상 사용하여 형성된 기본 키입니다. 예로는 사용자의 이메일 주소, 책의 ISBN 또는 사회 보장 번호가 있습니다.
 - 대리 키(Surrogate Key): 데이터베이스 외부에서는 의미가 없는 인공적인 시스템 생성 기본 키입니다. UUID 및 자동 증가 BIGINT는 대리 키의 일반적인 예입니다.
 - 분산 시스템(Distributed Systems): 구성 요소가 네트워크화된 다른 컴퓨터에 위치하며 메시지 통과를 통해 통신하고 작업을 조정하는 시스템입니다. 이 환경은 종종 고유성과 일관성을 유지하는 데 어려움을 야기합니다.
 - 인덱스 조각화(Index Fragmentation): 디스크의 데이터 물리적 저장소가 시간이 지남에 따라 구성이 해제되어 데이터 검색 속도가 느려지는 현상입니다. 이는 행이 삽입, 업데이트 또는 삭제될 때, 특히 비순차적 기본 키의 경우 발생할 수 있습니다.
 
기본 키 전쟁
각 기본 키 전략을 상세히 탐색하고, 원칙, 구현 및 이상적인 사용 사례를 검토해 봅시다.
자동 증가 BIGINT
원칙: BIGINT는 일반적으로 순차적인 자동 증가 정수입니다. 각 새 레코드에는 다음 사용 가능한 번호가 할당됩니다. 이것은 가장 전통적이고 종종 가장 간단한 접근 방식입니다.
구현:
CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
장점:
- 컴팩트한 저장 공간: BIGINT(8바이트)는 UUID(16바이트)보다 작으므로 저장 공간 오버헤드가 줄어들고 캐시에 더 많은 레코드를 저장할 수 있습니다.
 - B-트리 인덱스에 대한 뛰어난 성능: 순차적 삽입은 새 데이터를 끝에 추가하여 B-트리 인덱스 성능을 최적화하여 페이지 분할 및 조각화를 최소화합니다. 이를 통해 빠른 조회와 효율적인 캐시 사용이 가능합니다.
 - 가독성: 간단하고 순차적인 숫자는 사람이 읽고, 디버그하고, 참조하기 쉽습니다.
 - 자연스러운 정렬: ID를 기준으로 삽입 시간에 따라 데이터를 자연스럽게 정렬할 수 있습니다.
 
단점:
- 확장성 문제(분산 시스템): 분산 시스템에서 여러 독립적인 데이터베이스 인스턴스에 걸쳐 고유하고 순차적인 ID를 생성하는 것은 복잡합니다. 종종 중앙 집중식 ID 생성 서비스(예: Snowflake, Twitter의 ID 생성기)가 필요하며, 이는 단일 실패 지점 또는 지연 시간을 유발할 수 있습니다.
 - 예측 가능성/보안 문제: ID 시퀀스를 알면 공격자가 레코드를 추측하거나 반복할 수 있습니다. 주요 보안 조치는 아니지만 고려 사항입니다.
 - 데이터 마이그레이션 문제: 자동 증가 ID를 사용하는 다른 데이터베이스에서 데이터를 병합하면 ID 충돌이 발생할 수 있으며, 복잡한 매핑 또는 재 생성이 필요합니다.
 - 공급업체 종속성(암시적): 엄격한 공급업체 종속성은 아니지만, 특정 
AUTO_INCREMENT구문은 데이터베이스마다 약간 다를 수 있습니다. 
사용 사례: ID 생성을 처리하는 단일 중앙 데이터베이스가 있거나 외부 서비스를 통해 분산 ID 생성이 명시적으로 관리되는 모놀리식 애플리케이션 또는 시스템에 이상적입니다. 순차적 쓰기가 유익한 고부하 삽입 시나리오에 탁월합니다.
UUID
원칙: UUID는 전역적으로 고유하도록 설계된 128비트 숫자입니다. 다양한 버전(v1, v4, v7)이 있으며, 각 버전은 다른 생성 메커니즘을 가지고 있습니다. v4는 순전히 임의이며, v1은 MAC 주소와 타임스탬프를 포함하고, v7은 타임스탬프와 임의의 비트를 결합하여 v4보다 더 나은 데이터베이스 성능을 제공합니다.
구현:
-- PostgreSQL의 경우(uuid-ossp 확장을 사용하는 경우) CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE products ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, price NUMERIC(10, 2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- MySQL의 경우(UUID() 함수 또는 애플리케이션에서 생성) CREATE TABLE orders ( id BINARY(16) DEFAULT (UUID_TO_BIN(UUID(), 1)) PRIMARY KEY, -- 효율성을 위해 BINARY(16)으로 저장 user_id UUID, total_amount NUMERIC(10, 2), order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
장점:
- 전역적 고유성: 조정 없이 모든 데이터베이스, 서버 및 지리적 위치에서 고유성을 보장합니다. 이는 분산 또는 마이크로서비스 아키텍처에서 상당한 이점입니다.
 - 확장성(분산 시스템): ID는 충돌 없이 모든 서비스 또는 데이터베이스 인스턴스에서 독립적으로 생성될 수 있으므로 멀티 마스터, 멀티 테넌트 또는 연합 데이터베이스 설정에 적합합니다.
 - 클라이언트 측 생성: 데이터베이스에 저장하기 전에 클라이언트 또는 애플리케이션 계층에서 ID를 생성할 수 있어 오프라인 데이터 입력 또는 낙관적 잠금 전략을 단순화합니다.
 - 난독화를 통한 보안: UUID는 추측하거나 열거하기 어려워 사소한 수준의 난독화를 제공합니다.
 - 쉬운 데이터 병합: 다른 소스의 데이터셋을 ID 충돌 없이 결합할 수 있습니다.
 
단점:
- 저장 공간 오버헤드: UUID는 16바이트로 BIGINT의 두 배 크기이며, 더 큰 인덱스와 데이터 크기로 이어집니다.
 - B-트리 인덱스 조각화(임의 UUID): 임의 UUID(v4 등)는 비순차적 삽입으로 이어져 B-트리 인덱스에서 빈번한 페이지 분할 및 재조정을 일으켜 상당한 인덱스 조각화, I/O 증가 및 시간이 지남에 따라 쓰기/읽기 성능 저하를 초래합니다. 시간 순서대로 정렬된 ID(예: UUID v1 또는 v7)의 경우 이 문제는 덜 발생합니다.
 - 낮은 캐시 지역성: 임의 UUID는 관련 데이터가 디스크에 흩어져 캐시 성능을 저하시킬 수 있습니다.
 - 인간 가독성 저하: 길고 16진수 문자열은 읽고, 기억하고, 디버깅하기 번거롭습니다.
 - 조인 성능 영향: 더 큰 키 크기는 더 많은 데이터를 비교해야 하므로 조인 성능에 약간의 영향을 미칠 수 있습니다.
 
사용 사례: 분산 시스템, 마이크로서비스 아키텍처, 멀티 마스터 데이터베이스 복제 또는 ID를 오프라인 또는 독립 서비스에서 생성해야 하는 시나리오에 필수적입니다. 시간 순서대로 정렬된 UUID(예: v1, v7 또는 MySQL의 UUID_TO_BIN(UUID(), 1)) 사용은 전역적 고유성과 개선된 데이터베이스 성능의 이점을 결합하기 위해 강력히 권장됩니다.
자연 키
원칙: 자연 키는 사용자의 이메일 주소 또는 책의 ISBN과 같이 레코드를 본질적으로 식별하는 속성(또는 속성 집합)을 사용합니다.
구현:
CREATE TABLE customers ( email VARCHAR(255) PRIMARY KEY, -- 이메일을 자연 키로 사용 first_name VARCHAR(255), last_name VARCHAR(255), registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 복합 자연 키 예제 CREATE TABLE course_enrollments ( student_id BIGINT, course_code VARCHAR(10), enrollment_date DATE, PRIMARY KEY (student_id, course_code) );
장점:
- 비즈니스 의미: 키는 사용자와 비즈니스 도메인에 의미가 있습니다.
 - 중복 없음(잠재적): 자연 키가 이미 고유 식별자로 저장되어 있다면, 이를 PK로 사용하면 추가 대리 키 열 생성을 피할 수 있습니다.
 - 조인 간소화: 자연 키가 테이블 간에 직접 공유되는 경우 조인이 더 직관적일 수 있습니다.
 
단점:
- 변경 가능성 문제: 자연 키는 변경될 수 있습니다(예: 사용자의 이메일 주소). 기본 키가 변경되면 모든 관련 외래 키 테이블에 걸쳐 연쇄 업데이트가 필요하며, 이는 계산 비용이 많이 들고 관리하기 복잡하며 잠재적으로 데이터 불일치를 초래할 수 있습니다.
 - 데이터 무결성 문제: 자연 키가 항상 고유하거나 시간이 지남에 따라 모든 가능한 시나리오에서 일정하게 유지된다는 보장이 없을 수 있습니다. 오늘 고유해 보이는 것이 내일 그렇지 않을 수 있습니다.
 - 저장 공간 오버헤드: 자연 키가 긴 문자열(예: 이메일)인 경우 BIGINT보다 크고 더 많은 저장 공간을 소비하며 인덱스 성능에 영향을 줄 수 있습니다.
 - 개인 정보 문제: 자연 키에는 종종 민감한 정보(예: 이메일 주소, 주민등록번호)가 포함되어 있어 널리 노출되는 식별자로 사용하는 것이 바람직하지 않을 수 있습니다.
 - 복잡한 복합 키: 때로는 자연 키가 고유성을 보장하기 위해 여러 열(복합 키)이 필요하며, 이는 외래 키 관계 및 인덱싱을 복잡하게 만듭니다.
 - 개발 오버헤드: 기본 키의 업데이트를 처리하고 시스템 전체에서 참조 무결성을 보장하는 것은 상당한 개발 및 유지보수 오버헤드를 추가합니다.
 
사용 사례: 변경이 거의 없는 개체에는 고려될 수 있지만(예: 국가 코드, 고정 참조 데이터) 대부분의 현대 웹 애플리케이션에서는 강력히 권장되지 않습니다. 고유하고 안정적이며 간결한 자연 식별자가 있는 진정으로 불변하는 개체에 대해서는 고려할 수 있습니다. 거의 항상 대리 키가 선호됩니다.
결론
기본 키 선택은 현대 웹 애플리케이션에 광범위한 영향을 미치는 기본적인 결정입니다. 대부분의 시나리오에서 자동 증가 BIGINT는 중앙 집중식 ID 생성 메커니즘이 가능한 시스템에 대해 뛰어난 성능과 단순성을 제공합니다. 저장 공간을 최소화하고, B-트리 인덱싱을 최적화하며, 인간 친화적입니다. 그러나 분산 시스템에서는 조정 없이 전역적 고유성을 유지하는 것이 중요한 문제가 되므로 이것이 약점입니다.
이곳이 바로 UUID가 빛나는 지점입니다. 전역적 고유성, 독립적인 생성 및 분산 아키텍처에 대한 적합성은 마이크로서비스, 다중 지역 배포 및 멀티 테넌트 애플리케이션에 필수적인 선택이 됩니다. 기본 단점인 인덱스 조각화를 완화하기 위해 개발자는 전역적 고유성과 개선된 데이터베이스 성능의 이점을 결합하기 위해 시간 순서대로 정렬된 UUID 버전(예: v1, v7 또는 MySQL의 UUID_TO_BIN(UUID(), 1)과 같은 유사한 데이터베이스별 구현)을 우선적으로 사용해야 합니다.
자연 키는 개념적으로 비즈니스 의미 때문에 매력적이지만, 현대적이고 동적인 웹 애플리케이션에서 기본 키로는 지속 가능한 선택이 되기에는 변경 가능성, 무결성 및 개인 정보와 관련된 너무 많은 실제적인 문제를 제시합니다. 대리 키(BIGINT 또는 UUID)는 거의 항상 더 강력하고 유지 관리 가능한 기반을 제공합니다.
궁극적으로 결정 요인은 애플리케이션의 특정 아키텍처와 확장성 요구 사항입니다. 단일 데이터베이스를 갖춘 간단한 모놀리식 애플리케이션의 경우 BIGINT로 충분한 경우가 많습니다. 복잡하고 분산된 고확장성 시스템의 경우 UUID(특히 시간 순서대로 정렬된 변형)는 필요한 유연성과 복원력을 제공하여 기본 키 무기고에서 선호되는 무기가 됩니다.