This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
# 다중 프로젝트 피드백 플랫폼의 ID 및 성능 설계
|
||||
|
||||
## 핵심 결론
|
||||
|
||||
현재 시스템은 프로젝트마다 작성 서버가 따로 있고, 각 서버가 중앙 관리 API를 통해 하나의 DB에 글을 저장하는 구조임.
|
||||
|
||||
이 구조에서는 중앙 DB가 번호를 발급하기를 기다리는 방식보다, **각 작성 서버가 글 ID를 먼저 만들고 중앙 API로 보내는 방식**이 적합함.
|
||||
|
||||
### 최종 권장안
|
||||
|
||||
1. **1순위: ULID 또는 UUID v7 선발급**
|
||||
- 프로젝트 작성 서버에서 ID를 먼저 생성함
|
||||
- 화면 ID, API ID, DB ID를 동일하게 사용할 수 있음
|
||||
- 네트워크 재시도에도 같은 글로 판단해 중복 등록을 막을 수 있음
|
||||
- 여러 서버에 별도 Worker 번호를 배정하지 않아도 됨
|
||||
|
||||
2. **숫자 ID가 반드시 필요할 때: Snowflake**
|
||||
- 여러 서버에서 숫자 ID를 선발급할 수 있음
|
||||
- ULID보다 저장 공간이 작음
|
||||
- 대신 Worker ID와 서버 시간 동기화 관리가 필요함
|
||||
|
||||
3. **기존 구조를 가장 적게 바꿀 때: BIGINT Auto Increment**
|
||||
- 구현과 인덱스 성능은 가장 단순함
|
||||
- 중앙 DB 의존성과 재시도 중복을 별도로 해결해야 함
|
||||
- 화면 ID와 DB ID가 달라질 수 있음
|
||||
|
||||
### 선택 기준 한눈에 보기
|
||||
|
||||
| 우선순위 | 선택 |
|
||||
|---|---|
|
||||
| 여러 작성 서버의 독립성, 재시도 중복 방지, ID 일치 | ULID / UUID v7 |
|
||||
| 숫자 ID, 분산 선발급, 높은 쓰기량 | Snowflake |
|
||||
| 변경 최소화, 단일 DB, 중앙 저장 완료 후 ID 발급 | BIGINT Auto Increment |
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 시스템 구조
|
||||
|
||||
```text
|
||||
[프로젝트 A 작성 서버] ─┐
|
||||
[프로젝트 B 작성 서버] ─┼─ API ─▶ [중앙 관리 서버] ─▶ [중앙 MySQL]
|
||||
[프로젝트 C 작성 서버] ─┘
|
||||
```
|
||||
|
||||
- 피드백 작성 페이지 서버는 프로젝트마다 별도로 운영함
|
||||
- 각 작성 서버가 중앙 관리 API를 호출함
|
||||
- 중앙 관리 서버가 피드백을 한곳에 저장하고 관리함
|
||||
- 향후 외부 Q&A 서버도 같은 API로 연결할 수 있음
|
||||
|
||||
여러 서버가 데이터를 만들고 하나의 서버가 모아 관리하는 **다중 발행자·단일 집계 구조**임.
|
||||
|
||||
## 2. 현재 구조에서 발생하는 문제
|
||||
|
||||
### 2.1 네트워크 재시도에 따른 중복 등록
|
||||
|
||||
```text
|
||||
1. 프로젝트 A가 중앙 API로 글을 전송함
|
||||
2. 중앙 서버는 저장했지만 응답이 네트워크 지연으로 늦어짐
|
||||
3. 프로젝트 A는 실패로 판단하고 같은 글을 다시 전송함
|
||||
4. 중앙 DB가 매번 새 번호를 발급하면 같은 글이 2건 저장됨
|
||||
```
|
||||
|
||||
작성 서버가 ID를 먼저 만들고 재시도할 때 같은 ID를 사용하면 중복을 막을 수 있음.
|
||||
|
||||
```text
|
||||
같은 글 + 같은 ID = 같은 요청
|
||||
```
|
||||
|
||||
중앙 API와 DB에는 해당 ID를 PK 또는 유일 키로 설정해야 함. 이를 멱등성(Idempotency)이라고 함.
|
||||
|
||||
### 2.2 중앙 DB 번호 발급 의존
|
||||
|
||||
Auto Increment 방식에서는 중앙 DB가 저장하면서 번호를 발급함.
|
||||
|
||||
- 작성 서버는 저장이 끝나야 글 번호를 알 수 있음
|
||||
- 여러 프로젝트의 등록 요청이 중앙 DB에 집중됨
|
||||
- 중앙 DB 또는 네트워크 장애가 번호 발급과 저장에 함께 영향을 줌
|
||||
- 비동기 전송과 재시도 처리가 복잡해짐
|
||||
|
||||
### 2.3 프로젝트 구분
|
||||
|
||||
중앙 목록에서 숫자만 표시하면 출처 프로젝트를 바로 알기 어려움.
|
||||
|
||||
```text
|
||||
PRJA_01K8A9V4N5J9F28D5G3H1A2B3C
|
||||
PRJB_01K8A9V4N5J9F28D5G3H1A2B3D
|
||||
```
|
||||
|
||||
화면 ID와 DB ID를 일치시키려면 위의 전체 식별자를 DB에 그대로 저장해야 함. 프로젝트 코드와 ULID를 분리 저장한 뒤 화면에서 조합하면 화면 ID와 DB ID가 달라질 수 있음.
|
||||
|
||||
## 3. 식별자 선택 기준
|
||||
|
||||
| 기준 | 확인할 내용 |
|
||||
|---|---|
|
||||
| 중복 방지 | 네트워크 재시도에도 같은 글로 인식되는지 |
|
||||
| 선발급 | 중앙 DB 저장 전에 작성 서버에서 ID를 만들 수 있는지 |
|
||||
| 분산 생성 | 여러 서버가 동시에 만들어도 충돌하지 않는지 |
|
||||
| 정렬 성능 | 시간 순 생성으로 B-Tree 인덱스에 유리한지 |
|
||||
| 저장 크기 | PK와 외래키 인덱스가 과도하게 커지지 않는지 |
|
||||
| 화면 가독성 | 사람이 읽고 프로젝트를 구분하기 쉬운지 |
|
||||
| 일관성 | DB, API, URL, 화면 ID가 같은지 |
|
||||
| 운영 난이도 | 서버별 추가 설정과 관리가 필요한지 |
|
||||
|
||||
## 4. 식별자 체계 7가지 비교
|
||||
|
||||
| 번호 | 방식 | 저장 스펙 | 인덱스 특성 | 장점 | 단점 | 적합한 환경 |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| 1 | `INT AUTO_INCREMENT` | `INT` 4바이트 | 작고 빠름 | 구현이 가장 쉬움, 메모리 효율이 좋음 | 약 21억 개 한계, 중앙 DB 의존, 프로젝트 구분 어려움 | 소규모 단일 서버 |
|
||||
| 2 | `BIGINT AUTO_INCREMENT` | `BIGINT` 8바이트 | 순차 입력에 유리 | 번호 범위가 매우 큼, 구현이 쉬움 | 중앙 DB에서 번호 발급, 프로젝트 구분 어려움 | 단일 DB 기반 대용량 시스템 |
|
||||
| 3 | `BIGINT + 프로젝트 코드 조합` | DB는 `BIGINT` | 정수 인덱스 유지 | 저장 성능과 화면 식별성을 함께 확보 | 실제 ID와 화면 조합 ID가 달라질 수 있음 | 중앙 DB 구조와 운영 편의성을 함께 중시하는 경우 |
|
||||
| 4 | 비즈니스 문자열 직접 저장 | `VARCHAR` | 정수보다 큼 | DB만 봐도 프로젝트와 글을 구분 가능 | 문자열 인덱스 증가, 동시 채번 문제 | 트래픽이 적고 가독성이 중요한 경우 |
|
||||
| 5 | `ULID / UUID v7` | `BINARY(16)` 또는 문자열 | 시간 순 입력에 유리 | 서버별 선발급, 분산 생성, 재시도 멱등성에 적합 | 정수보다 크고 사람이 읽기 어려움 | 여러 프로젝트 서버가 API로 전송하는 구조 |
|
||||
| 6 | Snowflake | `BIGINT` 8바이트 | 시간 순 입력에 유리 | 분산 서버에서 숫자 ID를 선발급할 수 있음 | Worker ID와 시계 동기화 관리 필요 | 매우 높은 쓰기량의 분산 시스템 |
|
||||
| 7 | UUID v4 | UUID 또는 `VARCHAR(36)` | 랜덤 입력으로 페이지 분할 가능 | 생성이 쉽고 추측이 어려움 | 인덱스가 크고 랜덤 삽입으로 쓰기 효율이 낮아질 수 있음 | 보안상 추측 방지가 최우선인 경우 |
|
||||
|
||||
### 대략적인 PK 인덱스 크기
|
||||
|
||||
1,000만 건 기준의 대략적인 비교임. 실제 크기는 외래키, 보조 인덱스, 페이지 여유 공간에 따라 달라짐.
|
||||
|
||||
| 방식 | 대략적인 크기 |
|
||||
|---|---:|
|
||||
| `INT` | 약 200MB |
|
||||
| `BIGINT` | 약 400MB |
|
||||
| `BINARY(16)` | 약 800MB 이상 |
|
||||
| `VARCHAR(36)` UUID | 약 1~2GB 이상 |
|
||||
|
||||
## 5. 현재 시스템에 맞는 3가지 케이스
|
||||
|
||||
### Case 1. ULID 또는 UUID v7을 실제 글 ID로 사용
|
||||
|
||||
#### 구성
|
||||
|
||||
```text
|
||||
DB ID / API ID / 화면 ID:
|
||||
PRJA_01K8A9V4N5J9F28D5G3H1A2B3C
|
||||
```
|
||||
|
||||
- 각 프로젝트 작성 서버가 ID를 먼저 생성함
|
||||
- 글 데이터와 ID를 중앙 API로 전송함
|
||||
- 중앙 DB는 ID를 PK 또는 UNIQUE KEY로 확인함
|
||||
- 동일 ID의 재요청은 중복 저장하지 않음
|
||||
- 관리페이지는 전달받은 ID를 그대로 표시함
|
||||
|
||||
#### 장점
|
||||
|
||||
- 프로젝트 서버가 중앙 DB에 의존하지 않고 ID를 선발급할 수 있음
|
||||
- 네트워크 타임아웃 후 재시도해도 중복 등록을 막기 쉬움
|
||||
- 시간 순 ID라 UUID v4보다 B-Tree 인덱스에 유리함
|
||||
- 서버별 Worker ID를 배정할 필요가 없음
|
||||
- 화면 ID와 DB ID를 동일하게 유지할 수 있음
|
||||
|
||||
#### 단점
|
||||
|
||||
- `BIGINT`보다 PK와 외래키 인덱스가 큼
|
||||
- 사람이 숫자 하나로 기억하거나 구두 전달하기 어려움
|
||||
- 프로젝트 접두사를 포함하면 ID가 더 길어짐
|
||||
|
||||
#### 권장 저장 방식
|
||||
|
||||
화면 ID와 DB ID를 완전히 일치시켜야 하면 접두사와 ULID를 합친 전체 문자열을 `VARCHAR`에 저장함.
|
||||
|
||||
```text
|
||||
PRJA_01K8A9V4N5J9F28D5G3H1A2B3C
|
||||
```
|
||||
|
||||
DB 용량을 줄이기 위해 ULID를 `BINARY(16)`으로 분리 저장하면 화면에서 조합한 값과 DB의 실제 값이 달라질 수 있음.
|
||||
|
||||
### Case 2. Snowflake 숫자 ID 사용
|
||||
|
||||
#### 구성
|
||||
|
||||
```text
|
||||
DB ID / API ID / 화면 ID:
|
||||
178293849182394880
|
||||
```
|
||||
|
||||
Snowflake는 다음 값을 조합해 64비트 숫자를 생성함.
|
||||
|
||||
```text
|
||||
시간 정보 + Worker ID + 같은 시간 안의 순번
|
||||
```
|
||||
|
||||
#### 장점
|
||||
|
||||
- 여러 서버에서 중앙 DB 의존 없이 숫자 ID를 선발급할 수 있음
|
||||
- ULID보다 PK와 외래키 인덱스가 작음
|
||||
- 시간 순 정렬이 가능함
|
||||
- 화면 ID와 DB ID를 동일하게 유지할 수 있음
|
||||
|
||||
#### 단점
|
||||
|
||||
- 프로젝트와 서버마다 고유 Worker ID를 배정해야 함
|
||||
- 서버가 늘어나거나 컨테이너가 추가될 때 번호 관리가 필요함
|
||||
- Worker ID가 중복되면 ID 충돌이 발생함
|
||||
- 서버 시간 오차를 관리해야 함
|
||||
- 현재 등록량에서는 운영 복잡도가 성능 이점보다 클 수 있음
|
||||
|
||||
### Case 3. BIGINT Auto Increment + 프로젝트 코드 별도 표시
|
||||
|
||||
#### 구성
|
||||
|
||||
```text
|
||||
DB ID: 10492
|
||||
프로젝트: ABC
|
||||
채널: WEB
|
||||
화면 표시: ABC-WEB-10492
|
||||
```
|
||||
|
||||
- DB: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`
|
||||
- 프로젝트와 채널은 별도 컬럼으로 저장함
|
||||
- 화면과 알림에서 프로젝트 코드와 숫자를 조합함
|
||||
|
||||
#### 장점
|
||||
|
||||
- 정수 PK와 외래키로 인덱스가 작음
|
||||
- 기존 구조를 가장 적게 변경함
|
||||
- 중앙 DB에서 구현하기 쉬움
|
||||
- 순차 입력으로 B-Tree 페이지 분할이 적음
|
||||
|
||||
#### 단점
|
||||
|
||||
- 중앙 DB에 저장되어야 ID를 알 수 있음
|
||||
- 재시도 중복을 막으려면 별도의 Idempotency Key가 필요함
|
||||
- 화면의 `ABC-WEB-10492`와 DB의 `10492`가 다름
|
||||
- 프로젝트별 번호가 1번부터 시작하지 않음
|
||||
|
||||
## 6. ULID와 Snowflake를 쉽게 비교
|
||||
|
||||
### Snowflake
|
||||
|
||||
각 지점에 번호표 기계를 설치하고 지점 번호를 미리 배정하는 방식임.
|
||||
|
||||
- 프로젝트 A 서버는 Worker ID 1
|
||||
- 프로젝트 B 서버는 Worker ID 2
|
||||
- 프로젝트 C 서버는 Worker ID 3
|
||||
|
||||
번호표가 작고 빠르지만 서버가 늘어날 때마다 번호를 관리해야 함. 서버 시계가 맞지 않으면 번호 발급에 문제가 생길 수 있음.
|
||||
|
||||
### ULID
|
||||
|
||||
각 지점이 별도 등록 없이 앱을 실행해 고유 번호표를 만드는 방식임.
|
||||
|
||||
- Worker ID를 따로 배정하지 않아도 됨
|
||||
- 여러 서버에서 동시에 생성해도 충돌 가능성이 매우 낮음
|
||||
- 네트워크가 끊겨 같은 글을 다시 보내도 같은 ID로 중복을 확인할 수 있음
|
||||
- 시간 순으로 생성되어 DB에 정리하기 쉬움
|
||||
|
||||
현재처럼 프로젝트별 작성 서버가 여러 곳에 나뉘어 있으면 관리할 설정이 적은 ULID가 운영 측면에서 유리함.
|
||||
|
||||
## 7. 피드백 테이블 인덱스 설계
|
||||
|
||||
### 주요 조회 조건
|
||||
|
||||
```sql
|
||||
WHERE channel_id = ?
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
```
|
||||
|
||||
### 기본 후보
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_feedbacks_channel_active_list
|
||||
ON feedbacks (channel_id, deleted_at, id DESC);
|
||||
```
|
||||
|
||||
`deleted_at`을 포함한 이유는 소프트 삭제된 글을 목록에서 제외하는 조건까지 인덱스 탐색 범위에 포함하기 위해서임.
|
||||
|
||||
다만 삭제된 글이 거의 없으면 다음 인덱스가 더 작고 효율적일 수도 있음.
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_feedbacks_channel_list
|
||||
ON feedbacks (channel_id, id DESC);
|
||||
```
|
||||
|
||||
두 방식 중 어느 쪽이 좋은지는 삭제 데이터 비율과 실제 실행 계획으로 확인해야 함.
|
||||
|
||||
### 미확인 피드백 조회
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_feedbacks_unread
|
||||
ON feedbacks (channel_id, admin_first_read_at, deleted_at);
|
||||
```
|
||||
|
||||
### JSON 내부 값 조회
|
||||
|
||||
`data` 내부의 `category`를 자주 검색한다면 생성 컬럼과 인덱스를 구성할 수 있음.
|
||||
|
||||
```sql
|
||||
ALTER TABLE feedbacks
|
||||
ADD COLUMN category VARCHAR(50)
|
||||
GENERATED ALWAYS AS (data->>'$.category') VIRTUAL;
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_feedbacks_channel_category
|
||||
ON feedbacks (channel_id, category, id DESC);
|
||||
```
|
||||
|
||||
## 8. 5만 건 기준 메모리와 캐시
|
||||
|
||||
현재 523행의 데이터 용량이 약 `528.0 KiB`라면 다음과 같이 추정할 수 있음.
|
||||
|
||||
| 항목 | 예상 크기 |
|
||||
|---|---:|
|
||||
| 5만 건 데이터 | 약 50~70MB |
|
||||
| 복합 인덱스 2~3개 | 약 4~6MB |
|
||||
| 데이터와 인덱스 합계 | 약 80MB 미만 |
|
||||
|
||||
실제 크기는 JSON 또는 TEXT 데이터 길이, 인덱스 자료형, 보조 인덱스 수에 따라 달라짐.
|
||||
|
||||
### InnoDB Buffer Pool
|
||||
|
||||
```text
|
||||
[MySQL 데이터와 인덱스]
|
||||
│
|
||||
▼
|
||||
[InnoDB Buffer Pool 메모리 상주]
|
||||
│
|
||||
▼
|
||||
[반복 조회 시 디스크 접근 감소]
|
||||
```
|
||||
|
||||
Buffer Pool이 데이터와 인덱스보다 충분히 크면 자주 조회되는 페이지가 메모리에 상주할 수 있음. 다만 실제 상주율과 응답 시간은 서버 메모리, 설정, 실행 계획, 동시 접속자 수에 따라 달라짐.
|
||||
|
||||
### Redis 애플리케이션 캐시
|
||||
|
||||
```text
|
||||
[클라이언트]
|
||||
│
|
||||
▼
|
||||
[Redis 캐시] ── HIT ──▶ 캐시 결과 반환
|
||||
│
|
||||
MISS
|
||||
▼
|
||||
[중앙 API] ──▶ [MySQL] ──▶ Redis 갱신
|
||||
```
|
||||
|
||||
우선 캐싱할 대상:
|
||||
|
||||
- 프로젝트·채널별 최신 피드백 목록
|
||||
- 각 목록의 첫 페이지
|
||||
- 전체 피드백 수와 상태별 개수
|
||||
- 답변 대기·미확인 요약 정보
|
||||
|
||||
캐시 키 예시:
|
||||
|
||||
```text
|
||||
feedback:project:{project_id}:channel:{channel_id}:page:1
|
||||
```
|
||||
|
||||
새 글 등록, 수정, 삭제가 발생하면 관련 캐시를 삭제하거나 갱신해야 함.
|
||||
|
||||
| 방식 | 설명 |
|
||||
|---|---|
|
||||
| 즉시 삭제 | 변경 시 관련 캐시를 바로 삭제함 |
|
||||
| 짧은 TTL | 30초~1분 후 자동 만료함 |
|
||||
| 조합 | 첫 페이지는 즉시 삭제하고 나머지는 TTL을 사용함 |
|
||||
|
||||
## 9. 성능 점검 방법
|
||||
|
||||
인덱스를 적용하기 전에 실제 실행 계획을 확인해야 함.
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM feedbacks
|
||||
WHERE channel_id = 1
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
다음 항목을 확인함.
|
||||
|
||||
- 예상한 인덱스가 선택되는지
|
||||
- `key`에 복합 인덱스가 표시되는지
|
||||
- `rows`가 과도하게 많지 않은지
|
||||
- 전체 테이블 스캔이 발생하지 않는지
|
||||
- `Using filesort` 또는 `Using temporary`가 발생하는지
|
||||
|
||||
인덱스 적용 전후의 실행 시간, 읽은 행 수, DB CPU와 디스크 I/O를 함께 비교해야 함.
|
||||
Reference in New Issue
Block a user