23 KiB
분산 시스템 도입 시점 및 PK 설계안 비교
문서 목적
본 문서는 다중 프로젝트·다중 RP에서 Q&A와 피드백을 수집하는 시스템을 기준으로 다음 두 가지 설계를 비교한다.
- 처음부터 분산 시스템을 고려하여 설계하는 방안
- 현재 중앙 DB 구조를 유지하고, 실제 병목 발생 시 분산 시스템으로 확장하는 방안
현재 저장소는 apps/api → 중앙 MySQL 구조이며, 공통 엔터티의 PK는 INT AUTO_INCREMENT이다. 스테이징 덤프의 feedbacks도 약 523건 수준이므로 현재는 PK 용량이나 DB 하드웨어가 병목인 단계가 아니다.
핵심 결론
| 구분 | 1안: 처음부터 분산 고려 | 2안: 단계적 분산 도입 |
|---|---|---|
| 내부 PK | BINARY(16) UUID v7 |
BIGINT UNSIGNED AUTO_INCREMENT |
| 외부 식별자 | PK와 동일한 UUID v7 | 별도 public_id UUID v7 |
| 다중 작성 서버 | 즉시 지원 | API에서 중앙 수집, 필요 시 확장 |
| 초기 비용 | 높음 | 낮음 |
| FK·인덱스 크기 | 큼 | 작음 |
| 향후 샤딩 | 쉬움 | public_id를 기준으로 단계적 전환 |
| 현재 시스템 적합성 | 미래 요구사항이 확정된 경우 | 현재 시스템에 권장 |
최종 추천은 2안이다.
내부 PK BIGINT UNSIGNED AUTO_INCREMENT
외부 ID UUID v7 → BINARY(16)
멱등 키 source_system + source_record_id UNIQUE
검색 MySQL 인덱스 + 필요 시 OpenSearch
분산 도입 실제 Primary 병목 발생 시 단계별 확장
연간 5만 건 기준 하드웨어 검토
연간 피드백·댓글·이슈 등 전체 업무 레코드를 5만 건으로 가정한다.
| 항목 | 평균 규모 |
|---|---|
| 연간 레코드 | 50,000건 |
| 일평균 | 약 137건 |
| 시간당 평균 | 약 5.7건 |
| 초당 평균 쓰기 | 약 0.0016건 |
| 5년 누적 | 약 250,000건 |
| 10년 누적 | 약 500,000건 |
연간 5만 건 자체는 단일 MySQL Primary가 감당하지 못할 규모가 아니다. 데이터가 특정 이벤트에 몰리는 경우를 고려해도, 평균 쓰기량만으로 분산 DB를 선택할 근거는 부족하다.
만약 피드백 5만 건과 댓글 5만 건을 각각 의미한다면 연간 총 10만 건이지만, 이 정도 역시 연간 누적량만으로는 분산 DB 도입을 결정할 수준은 아니다. 동시 접속자와 순간 피크 쓰기량을 별도로 측정해야 한다.
예를 들어 평균 쓰기량의 1,000배가 짧은 시간에 발생해도 약 1.6건/초 수준이다. 다만 신규 글 등록보다 다음 항목이 먼저 병목이 될 수 있다.
- 동시에 접속한 관리자의 목록·통계 조회 수
- JSON 필드의 전문 검색과
%검색어%검색 - 매 요청마다 실행하는
COUNT(*) - OFFSET이 큰 페이지 조회
- 첨부파일 다운로드 트래픽
- 알림·웹훅·검색 색인 작업이 본 요청과 같은 트랜잭션에 묶이는 경우
첨부파일 본문은 DB 용량 계산에서 제외하고 Object Storage에 저장한다. 본문과 댓글 평균 크기를 210KB로 가정하면 5만 건의 원본 데이터는 대략 100500MB이고, 인덱스·레코드 오버헤드를 포함해도 일반적인 단일 DB에서 수년간 관리 가능한 범위다. 실제 크기는 JSON 본문과 인덱스 수를 측정해 확정해야 한다.
5만 건 기준 권장 구성
API 서버 2대 이상
MySQL Primary 1대
백업 또는 장애 복구용 Replica 1대
Object Storage: 첨부파일
OpenSearch: 전문 검색이 필요할 때만
이 규모에서는 샤딩이나 멀티 Primary보다 다음 순서가 적절하다.
BIGINT UNSIGNED내부 PK와 UUID v7public_id적용- 목록 조회용 복합 인덱스와 Keyset 페이지네이션 적용
- JSON 검색을 Generated Column 또는 OpenSearch로 분리
- 읽기 부하가 증가하면 Read Replica 추가
- 쓰기·알림·검색 색인을 Outbox와 큐로 분리
- 실제 Primary 포화 시에만 샤딩 검토
따라서 연간 5만 건을 전제로 하면 2안이 적합하다. 1안은 연간 건수 때문이 아니라 다중 DB 쓰기, 오프라인 생성, 멀티리전, 강한 장애 격리처럼 분산이 출시 필수 요구사항일 때 선택한다.
PK 변경 공수 비교
UUID를 외부 식별자로 추가하는 것과 UUID를 실제 PK로 사용하는 것은 변경 범위가 다르다.
비교표
| 항목 | BIGINT PK 유지 |
BIGINT PK + UUID public_id |
UUID v7 실제 PK |
|---|---|---|---|
| DB PK 변경 | 없음 | 없음 | 모든 PK/FK 변경 |
| FK 컬럼 | BIGINT |
BIGINT |
BINARY(16) |
| TypeORM Entity | 변경 적음 | 컬럼 추가 | ID 타입·생성 방식 전면 변경 |
| API 계약 | 기존 숫자 ID 유지 | public_id를 단계적 추가 | URL·DTO·응답 ID 변경 |
| 프론트엔드 | 변경 적음 | 필요한 화면부터 변경 | id: number 전면 변경 |
| 인덱스 크기 | 가장 작음 | 내부 인덱스는 동일 | PK·FK·보조 인덱스 증가 |
| 기존 데이터 변환 | 없음 | UUID backfill | PK·FK 매핑 및 제약조건 재생성 |
| 외부 연동 영향 | 낮음 | 낮음~중간 | 웹훅·검색·테스트·연동 영향 큼 |
| 분산 선발급 | 어려움 | public_id로 가능 | PK 자체로 가능 |
| 전체 공수 | 낮음 | 중간 | 높음 |
UUID를 실제 PK로 변경할 때 필요한 작업
현재 시스템은 공통 엔터티에서 id: number와 INT AUTO_INCREMENT를 사용하고 있으며, 댓글·첨부파일·통계·이슈 연결 테이블이 숫자 ID를 FK로 참조한다. 따라서 UUID PK 전환 시 다음 작업이 필요하다.
- 모든 핵심 테이블의 PK를
BINARY(16)으로 변경 - 모든 FK와 조인 테이블의 참조 컬럼을
BINARY(16)으로 변경 - TypeORM Entity의 ID 타입과 생성 로직 변경
- DTO의 숫자 검증을 UUID 검증으로 변경
- API URL과 응답의 ID 형식 변경
- 프론트엔드의
id: number타입과 정렬·비교 로직 변경 - 기존 Foreign Key와 인덱스 삭제·재생성
- 웹훅, OpenSearch 문서 ID, 테스트 fixture, E2E 데이터 변경
- UUID 생성·바이너리 변환·JSON 직렬화 규칙 추가
UUID v4보다 UUID v7을 사용하고, 저장 타입은 CHAR(36)이 아니라 BINARY(16)으로 통일한다.
권장 마이그레이션 방식
현재 구조에서는 기존 PK를 유지하면서 외부 UUID를 추가하는 방식이 안전하다.
ALTER TABLE feedbacks
ADD COLUMN public_id BINARY(16) NULL,
ADD UNIQUE KEY uk_feedback_public_id (public_id);
이후 다음 순서로 전환한다.
- 모든 기존 행에 UUID v7
public_id를 backfill - 신규 데이터부터
public_id를 동시에 생성 - API 응답과 URL에 public_id를 선택적으로 추가
- 외부 클라이언트가 public_id를 사용하도록 전환
source_system + source_record_id유일 키 추가- 충분한 호환 기간 후 숫자 ID의 외부 노출을 중단
이 방식은 내부 조인과 FK를 그대로 유지하면서도, 향후 API 수평 확장·다중 작성 서버·샤딩에서 UUID를 전역 식별자로 사용할 수 있게 한다.
공수 판단
BIGINT PK 유지 낮음
BIGINT PK + UUID public_id 추가 중간
UUID v7을 실제 PK로 전면 전환 높음
현재 스테이징 데이터가 약 523건이므로 데이터 backfill 자체는 어렵지 않다. 하지만 UUID 실제 PK 전환의 공수는 데이터 건수보다 Entity, FK, API, 프론트엔드, 테스트와 외부 연동 변경에서 발생한다.
따라서 UUID가 반드시 전역 PK여야 한다는 요구사항이 확정되지 않았다면, BIGINT 내부 PK + UUID v7 public_id를 기본 설계로 선택한다.
외부 식별자 방식 비교
public_id는 외부 API, URL, 웹훅, 로그에서 사용하는 식별자다. 내부 DB PK와 달리 다음 기준을 함께 고려해야 한다.
- 여러 서버에서 중앙 조율 없이 생성할 수 있는가
- 시간순 정렬이 가능한가
- URL과 API에서 사용하기 쉬운가
- DB 인덱스에 미치는 영향이 작은가
- 표준과 라이브러리 지원이 충분한가
- 생성 시간이나 순번이 과도하게 노출되지 않는가
| 방식 | 표현 길이 | 시간순 정렬 | 분산 생성 | 운영 복잡도 | 주요 단점 | 적합성 |
|---|---|---|---|---|---|---|
| UUID v7 | 36자 문자열 / 16바이트 | 가능 | 가능 | 낮음 | 생성 시간이 일부 노출됨 | 가장 균형적 |
| ULID | 26자 문자열 / 16바이트 | 가능 | 가능 | 낮음 | UUID 표준과 별도 사양 | 짧은 URL |
| Snowflake | 약 18~19자리 | 가능 | 가능 | 높음 | Worker ID와 시계 관리 필요 | 초고속 분산 시스템 |
| UUID v4 | 36자 문자열 / 16바이트 | 불가 | 가능 | 낮음 | 랜덤 삽입, 정렬 정보 없음 | 랜덤성과 추측 방지 |
UUID v7
UUID v7은 IETF RFC 9562에 정의된 표준 UUID 방식이다. 앞부분에 Unix millisecond timestamp를 포함하고 나머지 영역을 랜덤 또는 단조 증가 값으로 사용할 수 있다. RFC 9562
0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d
장점:
- UUID v4와 동일한 128비트 구조 및
BINARY(16)저장 방식 사용 - UUID v4보다 B-Tree 인덱스의 순차 삽입에 유리
- Worker ID를 별도로 배정하지 않아도 됨
- UUID 표준과 도구를 그대로 활용 가능
- 외부 ID로 사용해도 충돌 가능성이 매우 낮음
주의사항:
- 생성 시간이 밀리초 단위로 일부 노출됨
- 보안 토큰이나 비밀번호 재설정 토큰으로 사용하면 안 됨
- UUID 자체가 접근 권한을 대신하지 않음
ULID
ULID는 128비트 시간순 식별자이며, 표준 표현이 26자이고 Crockford Base32를 사용한다. UUID 문자열보다 짧고 URL에 넣기 편하다. ULID 공식 사양
01K8A9V4N5J9F28D5G3H1A2B3C
UUID v7과 기능적으로 매우 유사하지만, UUID 표준과는 별도의 사양이다. 외부 URL의 길이와 사람이 읽기 쉬운 형태를 가장 중요하게 생각하면 ULID가 더 적합할 수 있다.
Snowflake
Snowflake는 보통 다음 정보를 조합한 64비트 숫자 ID다.
timestamp + worker ID + sequence
인덱스와 FK가 UUID보다 작고 숫자 정렬도 빠르다. 그러나 서버별 Worker ID 할당, 오토스케일링 시 충돌 방지, 시계 역행 처리, JavaScript에서의 큰 정수 직렬화가 필요하다. X의 Snowflake ID 설명
연간 5만~10만 건 수준의 시스템에서는 저장 공간 절약보다 운영 복잡도가 더 커질 가능성이 높다.
UUID v4
UUID v4는 랜덤성이 가장 중요한 경우에 적합하다.
c9a646d3-9c61-4cc9-bc19-482386a37365
시간 정보가 없으므로 생성 순서로 정렬할 수 없고, 랜덤한 B-Tree 삽입으로 UUID v7보다 인덱스 효율이 떨어질 수 있다. 생성 시점 노출을 피해야 하는 외부 토큰에는 UUID v4가 더 적합할 수 있다.
이 시스템의 외부 식별자 선택
현재 시스템에서는 다음 구성이 적절하다.
DB 저장 BINARY(16)
생성 방식 UUID v7
API 응답 표준 UUID 문자열
URL UUID 문자열
화면 표시 축약 UUID + 복사 버튼
내부 조인 BIGINT PK
사람이 업무 화면에서 확인할 번호가 필요하다면 UUID를 표시 번호로 사용하지 않고 별도의 업무 번호를 둔다.
외부 식별자: 0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d
업무 표시번호: ABC-2026-000123
최종 선택 기준:
| 우선순위 | 선택 |
|---|---|
| 표준성·분산성·시간순 정렬의 균형 | UUID v7 |
| 짧은 URL과 가독성 | ULID |
| 초당 수천~수만 건의 분산 숫자 생성 | Snowflake |
| 시간 노출 없는 랜덤성 | UUID v4 |
따라서 이 시스템은 BIGINT 내부 PK + UUID v7 public_id 조합을 기본으로 하고, URL 길이가 최우선인 외부 서비스에서만 ULID를 대안으로 검토한다.
1안. 처음부터 분산 시스템을 고려하는 설계
1.1 목표
다음 요구사항이 이미 확정되어 있을 때 선택한다.
- 프로젝트별 작성 서버가 중앙 DB 장애와 독립적으로 ID를 생성해야 함
- 네트워크 단절 후 재시도에도 중복 등록을 방지해야 함
- 향후 여러 DB 샤드 또는 멀티리전 쓰기를 계획하고 있음
- 피드백·댓글·이슈가 서로 다른 저장소에 분산될 수 있음
- 외부 시스템과의 전역 식별자가 필요함
1.2 권장 ID 구조
모든 핵심 엔터티의 PK를 UUID v7 기반 BINARY(16)으로 통일한다.
feedback.id BINARY(16) PRIMARY KEY
feedback_comment.id BINARY(16) PRIMARY KEY
issue.id BINARY(16) PRIMARY KEY
attachment.id BINARY(16) PRIMARY KEY
UUID는 DB에서 생성하지 않고 작성 애플리케이션에서 생성한다.
[RP A 서버] ─┐
[RP B 서버] ─┼─ UUID v7 생성 → 중앙 API → DB 또는 메시지 큐
[RP C 서버] ─┘
같은 요청을 재전송할 때 작성 서버가 최초 생성한 UUID를 다시 보내면 중앙 API에서 멱등 처리를 할 수 있다.
1.3 권장 테이블 구조
CREATE TABLE feedbacks (
id BINARY(16) NOT NULL,
channel_id BINARY(16) NOT NULL,
data JSON NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
source_system VARCHAR(32) NULL,
source_record_id VARCHAR(128) NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_feedback_source (source_system, source_record_id),
KEY idx_feedback_channel_created (channel_id, created_at DESC, id DESC)
);
source_system과 source_record_id는 외부 RP의 원본 식별자다. UUID가 동일하지 않더라도 같은 원본 요청의 재전송을 차단할 수 있도록 별도 유일 키를 둔다.
1.4 서비스 구성
┌─ API 인스턴스 1 ─┐
[RP 서버들] ─────┼─ API 인스턴스 2 ─┼─ [메시지 큐/Outbox] ─ [MySQL Primary]
└─ API 인스턴스 N ─┘ │
├─ [Read Replica]
├─ [OpenSearch]
└─ [Object Storage]
구성 원칙:
- API 서버는 상태를 보유하지 않고 수평 확장한다.
- 저장 완료와 알림·검색 색인 작업을 분리한다.
- DB 트랜잭션 안에서 Outbox 이벤트를 저장한 뒤 비동기 발행한다.
- 첨부파일 본문은 DB가 아니라 S3/R2 호환 스토리지에 저장한다.
- 검색과 원본 데이터 저장을 분리한다.
- 샤딩 이후에는 서로 다른 DB 간 Foreign Key를 사용하지 않는다.
1.5 장점
- DB에 저장하기 전에도 전역 ID를 발급할 수 있다.
- 네트워크 재시도와 비동기 수집에 유리하다.
- 여러 DB 샤드에 데이터를 나누어도 ID 충돌이 없다.
- 프로젝트 서버가 추가되어도 Worker ID를 배정할 필요가 없다.
- 화면·API·로그에서 같은 식별자를 사용할 수 있다.
1.6 단점
- 모든 PK와 FK가 16바이트가 되어 인덱스와 Buffer Pool 사용량이 증가한다.
- TypeORM 엔터티, DTO, 프론트엔드의
number타입을 전반적으로 변경해야 한다. - UUID 변환 코드와 직렬화 규칙이 필요하다.
- 분산 트랜잭션, 이벤트 중복, 순서 보장 문제를 초기에 해결해야 한다.
- 실제 트래픽이 작으면 운영 복잡도만 증가할 수 있다.
1.7 이 안을 선택해야 하는 조건
다음 중 하나라도 출시 전에 확정되어 있다면 1안을 검토한다.
- 초기부터 여러 DB에 쓰기 작업을 분산해야 함
- 모바일·현장·오프라인 환경에서 중앙 DB 없이 글을 생성해야 함
- 리전별 데이터 저장 또는 테넌트별 DB 격리가 필수임
- 초기부터 글로벌 단일 식별자가 외부 계약으로 고정되어야 함
- 향후 PK 변경을 절대로 허용할 수 없음
하드웨어 용량 때문에 1안을 선택하려면 연간 건수가 아니라 예상 피크 부하를 기준으로 판단한다. 부하 테스트에서 단일 Primary의 CPU·I/O·Lock·응답 시간이 목표치를 초과하고, 수직 확장·인덱스·읽기 복제·비동기화로도 해결되지 않는다는 결과가 있어야 한다.
단순히 “언젠가 분산할 수 있다”거나 “연간 5만 건이 쌓인다”는 이유만으로 1안을 선택하는 것은 권장하지 않는다.
2안. 현재 구조 유지 후 병목 시 분산 시스템 도입
2.1 기본 설계
현재 중앙 MySQL 구조를 유지하되, 내부 PK와 외부 식별자를 분리한다.
내부 PK: BIGINT UNSIGNED AUTO_INCREMENT
외부 public_id: UUID v7 BINARY(16)
외부 원본 키: source_system + source_record_id
예시:
CREATE TABLE feedbacks (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
public_id BINARY(16) NOT NULL,
channel_id BIGINT UNSIGNED NOT NULL,
data JSON NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
source_system VARCHAR(32) NULL,
source_record_id VARCHAR(128) NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_feedback_public_id (public_id),
UNIQUE KEY uk_feedback_source (source_system, source_record_id),
KEY idx_feedback_channel_list (channel_id, created_at DESC, id DESC)
);
댓글·첨부파일·이슈 연결 테이블의 내부 FK는 BIGINT UNSIGNED로 통일한다.
2.2 단계별 도입 순서
단계 0. 단일 DB 최적화
INT를 신규 설계부터BIGINT UNSIGNED로 통일한다.feedbacks(channel_id, deleted_at, id)복합 인덱스를 추가한다.- OFFSET 페이지네이션을 Keyset 페이지네이션으로 변경한다.
- JSON에서 자주 검색하는 필드를 일반 컬럼 또는 Generated Column으로 분리한다.
- 목록 조회와
COUNT(*)를 분리하거나 count 캐시를 사용한다. %검색어%전문 검색은 OpenSearch로 이전한다.
권장 목록 조회:
SELECT ...
FROM feedbacks
WHERE channel_id = ?
AND deleted_at IS NULL
AND id < ?
ORDER BY id DESC
LIMIT 20;
단계 1. 읽기 확장
다음 순서로 읽기 부하를 분산한다.
MySQL Primary
├─ Read Replica 1
└─ Read Replica 2
- 쓰기·트랜잭션은 Primary로 보낸다.
- 목록·통계·검색용 조회는 Replica 또는 OpenSearch로 보낸다.
- 작성 직후 조회처럼 최신성이 필요한 요청은 Primary를 사용한다.
- Replica 지연을 모니터링한다.
단계 2. 애플리케이션 수평 확장
- API 서버를 무상태로 유지한다.
- 세션은 외부 저장소 또는 서명 토큰으로 관리한다.
- Connection Pool을 서버 수에 맞게 제한한다.
- 알림·웹훅·검색 색인은 큐 또는 Outbox로 분리한다.
단계 3. 쓰기 부하 분리
Primary 쓰기가 포화되기 시작하면 바로 샤딩하지 않고 다음 작업부터 수행한다.
- 통계 집계를 원본 트랜잭션과 분리한다.
- 알림·웹훅·검색 색인 작업을 비동기화한다.
- 이력·이벤트 테이블을 별도 저장소로 이동한다.
- 오래된 데이터는 보관 DB 또는 파티션으로 이동한다.
- 쓰기 트랜잭션의 Lock 범위와 인덱스를 점검한다.
단계 4. 실제 필요 시 샤딩
샤딩 키는 tenant_id, workspace_id, project_id 중 실제 데이터 격리 기준을 선택한다.
Shard Map
tenant/workspace → shard-1 또는 shard-2
이때 public_id는 샤드와 관계없이 전역 식별자로 유지한다. 샤드 간 JOIN과 Foreign Key는 사용하지 않고, API나 이벤트 계층에서 조회를 조합한다.
2.3 장점
- 현재 시스템의 변경 범위가 작다.
BIGINTPK와 FK로 인덱스·조인 성능을 유지한다.- 실제 병목에 맞춰 읽기·검색·쓰기 분산을 각각 도입할 수 있다.
- UUID v7
public_id를 지금 추가하므로 향후 샤딩 시 외부 식별자 변경이 없다. - 초기 운영과 장애 대응이 단순하다.
2.4 단점
- 내부
BIGINT AUTO_INCREMENT는 여러 DB에서 독립적으로 발급하기 어렵다. - 샤딩 시 내부 ID는 전역 식별자로 사용할 수 없다.
- 향후 샤딩 시 DB 간 Foreign Key를 제거해야 한다.
- 처음부터 다중 DB 쓰기가 필수라면 추가 전환 작업이 필요하다.
2.5 분산 도입 임계점
다음 조건이 피크 시간에 지속되고, 인덱스·쿼리·캐시·수직 확장으로 해결되지 않을 때 다음 단계로 이동한다.
| 측정 항목 | 검토 기준 |
|---|---|
| DB CPU | 70~80% 이상 지속 |
| 디스크 I/O 대기 | 10% 이상 |
| 읽기 p95 | 300ms 이상 |
| 쓰기 p95 | 500ms 이상 |
| Connection Pool | 80% 이상 |
| Buffer Pool 적중률 | 99% 미만 |
| Replica 지연 | 5~10초 이상 |
| Lock wait/deadlock | 지속적인 증가 |
| Primary 쓰기 용량 | 부하 테스트 최대치의 70~80% 초과 |
행 수는 참고 지표일 뿐 분산 도입의 직접 기준으로 사용하지 않는다.
PK 자료 유형 비교
| 방식 | 저장 | 중앙 DB 의존 | 분산 선발급 | 인덱스 효율 | 권장 용도 |
|---|---|---|---|---|---|
INT AUTO_INCREMENT |
4바이트 | 높음 | 불가 | 매우 좋음 | 기존 소규모 시스템 |
BIGINT UNSIGNED AUTO_INCREMENT |
8바이트 | 높음 | 불가 | 매우 좋음 | 현재 중앙 DB 내부 PK |
UUID v4 BINARY(16) |
16바이트 | 없음 | 가능 | 보통 | 보안 우선 식별자 |
UUID v7 BINARY(16) |
16바이트 | 없음 | 가능 | 좋음 | 분산·외부 식별자 |
| ULID | 16바이트 또는 문자열 | 없음 | 가능 | 좋음 | UUID v7 대안 |
Snowflake BIGINT |
8바이트 | 없음 | 가능 | 매우 좋음 | 초고속 분산 숫자 ID |
CHAR(36) UUID |
36바이트 | 없음 | 가능 | 낮음 | 비권장 |
UUID v4, UUID v7, ULID를 사용하더라도 문자열로 저장하지 말고 가능하면 BINARY(16)으로 저장한다.
두 안의 최종 선택 기준
1안을 선택하는 경우
초기부터 다중 DB 쓰기
또는 오프라인 생성
또는 멀티리전 저장
또는 전역 UUID PK가 외부 계약으로 확정
이 경우 UUID v7 BINARY(16)을 실제 PK로 사용한다.
2안을 선택하는 경우
현재는 중앙 MySQL
쓰기량이 크지 않음
운영 복잡도를 낮춰야 함
향후 분산 가능성만 있음
이 경우 BIGINT UNSIGNED를 내부 PK로 사용하고 UUID v7 BINARY(16)을 외부 식별자로 추가한다.
최종 추천
현재 시스템에는 2안을 적용한다.
feedbacks.id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
feedbacks.public_id BINARY(16) UNIQUE NOT NULL
feedbacks.source_system
feedbacks.source_record_id
UNIQUE(source_system, source_record_id)
이 설계는 현재의 인덱스·조인 성능을 유지하면서도, 향후 API 수평 확장·Read Replica·OpenSearch·Outbox·샤딩으로 확장할 수 있는 경로를 확보한다.
1안은 분산 요구사항이 기술적 가능성이 아니라 출시 전 확정된 필수 요구사항일 때 선택한다.