Files
baron_qa_write/docs/관리페이지 md 파일/post-id-strategy-and-performance.md
root 3c10478482
Deploy EG-BIM QA Gateway / deploy (push) Successful in 2m4s
관리페이지 데이터 전송 구현
2026-09-21 14:46:36 +09:00

14 KiB

다중 프로젝트 피드백 플랫폼의 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. 현재 시스템 구조

[프로젝트 A 작성 서버] ─┐
[프로젝트 B 작성 서버] ─┼─ API ─▶ [중앙 관리 서버] ─▶ [중앙 MySQL]
[프로젝트 C 작성 서버] ─┘
  • 피드백 작성 페이지 서버는 프로젝트마다 별도로 운영함
  • 각 작성 서버가 중앙 관리 API를 호출함
  • 중앙 관리 서버가 피드백을 한곳에 저장하고 관리함
  • 향후 외부 Q&A 서버도 같은 API로 연결할 수 있음

여러 서버가 데이터를 만들고 하나의 서버가 모아 관리하는 다중 발행자·단일 집계 구조임.

2. 현재 구조에서 발생하는 문제

2.1 네트워크 재시도에 따른 중복 등록

1. 프로젝트 A가 중앙 API로 글을 전송함
2. 중앙 서버는 저장했지만 응답이 네트워크 지연으로 늦어짐
3. 프로젝트 A는 실패로 판단하고 같은 글을 다시 전송함
4. 중앙 DB가 매번 새 번호를 발급하면 같은 글이 2건 저장됨

작성 서버가 ID를 먼저 만들고 재시도할 때 같은 ID를 사용하면 중복을 막을 수 있음.

같은 글 + 같은 ID = 같은 요청

중앙 API와 DB에는 해당 ID를 PK 또는 유일 키로 설정해야 함. 이를 멱등성(Idempotency)이라고 함.

2.2 중앙 DB 번호 발급 의존

Auto Increment 방식에서는 중앙 DB가 저장하면서 번호를 발급함.

  • 작성 서버는 저장이 끝나야 글 번호를 알 수 있음
  • 여러 프로젝트의 등록 요청이 중앙 DB에 집중됨
  • 중앙 DB 또는 네트워크 장애가 번호 발급과 저장에 함께 영향을 줌
  • 비동기 전송과 재시도 처리가 복잡해짐

2.3 프로젝트 구분

중앙 목록에서 숫자만 표시하면 출처 프로젝트를 바로 알기 어려움.

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로 사용

구성

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에 저장함.

PRJA_01K8A9V4N5J9F28D5G3H1A2B3C

DB 용량을 줄이기 위해 ULID를 BINARY(16)으로 분리 저장하면 화면에서 조합한 값과 DB의 실제 값이 달라질 수 있음.

Case 2. Snowflake 숫자 ID 사용

구성

DB ID / API ID / 화면 ID:
178293849182394880

Snowflake는 다음 값을 조합해 64비트 숫자를 생성함.

시간 정보 + Worker ID + 같은 시간 안의 순번

장점

  • 여러 서버에서 중앙 DB 의존 없이 숫자 ID를 선발급할 수 있음
  • ULID보다 PK와 외래키 인덱스가 작음
  • 시간 순 정렬이 가능함
  • 화면 ID와 DB ID를 동일하게 유지할 수 있음

단점

  • 프로젝트와 서버마다 고유 Worker ID를 배정해야 함
  • 서버가 늘어나거나 컨테이너가 추가될 때 번호 관리가 필요함
  • Worker ID가 중복되면 ID 충돌이 발생함
  • 서버 시간 오차를 관리해야 함
  • 현재 등록량에서는 운영 복잡도가 성능 이점보다 클 수 있음

Case 3. BIGINT Auto Increment + 프로젝트 코드 별도 표시

구성

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. 피드백 테이블 인덱스 설계

주요 조회 조건

WHERE channel_id = ?
  AND deleted_at IS NULL
ORDER BY id DESC
LIMIT 20

기본 후보

CREATE INDEX idx_feedbacks_channel_active_list
ON feedbacks (channel_id, deleted_at, id DESC);

deleted_at을 포함한 이유는 소프트 삭제된 글을 목록에서 제외하는 조건까지 인덱스 탐색 범위에 포함하기 위해서임.

다만 삭제된 글이 거의 없으면 다음 인덱스가 더 작고 효율적일 수도 있음.

CREATE INDEX idx_feedbacks_channel_list
ON feedbacks (channel_id, id DESC);

두 방식 중 어느 쪽이 좋은지는 삭제 데이터 비율과 실제 실행 계획으로 확인해야 함.

미확인 피드백 조회

CREATE INDEX idx_feedbacks_unread
ON feedbacks (channel_id, admin_first_read_at, deleted_at);

JSON 내부 값 조회

data 내부의 category를 자주 검색한다면 생성 컬럼과 인덱스를 구성할 수 있음.

ALTER TABLE feedbacks
ADD COLUMN category VARCHAR(50)
GENERATED ALWAYS AS (data->>'$.category') VIRTUAL;
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

[MySQL 데이터와 인덱스]
          │
          ▼
[InnoDB Buffer Pool 메모리 상주]
          │
          ▼
[반복 조회 시 디스크 접근 감소]

Buffer Pool이 데이터와 인덱스보다 충분히 크면 자주 조회되는 페이지가 메모리에 상주할 수 있음. 다만 실제 상주율과 응답 시간은 서버 메모리, 설정, 실행 계획, 동시 접속자 수에 따라 달라짐.

Redis 애플리케이션 캐시

[클라이언트]
     │
     ▼
[Redis 캐시] ── HIT ──▶ 캐시 결과 반환
     │
    MISS
     ▼
[중앙 API] ──▶ [MySQL] ──▶ Redis 갱신

우선 캐싱할 대상:

  • 프로젝트·채널별 최신 피드백 목록
  • 각 목록의 첫 페이지
  • 전체 피드백 수와 상태별 개수
  • 답변 대기·미확인 요약 정보

캐시 키 예시:

feedback:project:{project_id}:channel:{channel_id}:page:1

새 글 등록, 수정, 삭제가 발생하면 관련 캐시를 삭제하거나 갱신해야 함.

방식 설명
즉시 삭제 변경 시 관련 캐시를 바로 삭제함
짧은 TTL 30초~1분 후 자동 만료함
조합 첫 페이지는 즉시 삭제하고 나머지는 TTL을 사용함

9. 성능 점검 방법

인덱스를 적용하기 전에 실제 실행 계획을 확인해야 함.

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를 함께 비교해야 함.