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

16 KiB

다중 프로젝트 피드백 플랫폼의 ID 및 성능 설계

1. 현재 시스템 구조

현재 구조는 여러 프로젝트의 작성 서버가 하나의 중앙 관리 서버로 데이터를 보내는 형태임.

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

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

2. 이 구조에서 먼저 해결해야 할 문제

2.1 재시도에 따른 중복 등록

다음과 같은 상황이 발생할 수 있음.

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

따라서 글을 작성하는 서버가 중앙 DB에 보내기 전에 고유 ID를 먼저 만들고, 재시도할 때도 같은 ID를 사용해야 함.

같은 글 + 같은 ID = 같은 요청으로 판단

중앙 API와 DB에는 해당 ID를 기본 키 또는 유일 키로 설정해 중복 저장을 막아야 함. 이를 멱등성(Idempotency)이라고 함.

2.2 중앙 번호 발급에 대한 의존

Auto Increment를 사용하면 중앙 DB가 저장하면서 번호를 발급함.

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

2.3 프로젝트 구분

중앙 관리페이지에는 여러 프로젝트의 글이 함께 보이므로 숫자만 표시하면 어느 프로젝트에서 온 글인지 바로 알기 어려움.

예를 들어 다음과 같이 구분할 수 있음.

PRJA_01K8A9V4N5J9F28D5G3H1A2B3C
PRJB_01K8A9V4N5J9F28D5G3H1A2B3D

단, 화면 표시 ID와 DB에 저장된 식별자를 반드시 같게 유지하려면 위의 전체 문자열을 그대로 저장하는 방식과, DB에는 별도 값을 저장하고 화면에서 조합하는 방식을 구분해야 함.

3. 식별자 선택 시 확인할 기준

기준 확인할 내용
중복 방지 네트워크 재시도에도 같은 글로 인식되는지
선발급 중앙 DB 저장 전에 작성 서버에서 ID를 만들 수 있는지
분산 생성 여러 프로젝트 서버가 동시에 만들어도 충돌하지 않는지
정렬 성능 ID가 시간 순으로 생성되어 B-Tree에 유리한지
저장 크기 PK와 외래키 인덱스가 과도하게 커지지 않는지
화면 가독성 사람이 읽고 프로젝트를 구분하기 쉬운지
일관성 DB ID, API ID, URL ID, 화면 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) 랜덤 입력으로 페이지 분할 가능 생성이 쉽고 추측이 어려움 인덱스가 크고 랜덤 삽입으로 쓰기 효율이 낮아질 수 있음 보안상 추측 방지가 최우선인 경우

대략적인 저장 크기

1,000만 건의 단일 PK 인덱스를 기준으로 한 대략적인 비교임. 실제 크기는 PK 외래키, 보조 인덱스, 페이지 여유 공간에 따라 달라짐.

방식 대략적인 PK 인덱스 크기
INT 약 200MB
BIGINT 약 400MB
BINARY(16) 약 800MB 이상
VARCHAR(36) UUID 약 1~2GB 이상

5. ULID와 Snowflake 비교

두 방식 모두 중앙 DB에 저장하기 전에 각 작성 서버에서 ID를 만들 수 있음.

비교 항목 ULID / UUID v7 Snowflake
저장 크기 16바이트 중심 8바이트
시간 순 정렬 가능 가능
서버 선발급 가능 가능
서버별 설정 거의 없음 Worker ID 필수
시계 오차 영향 상대적으로 낮음 민감함
숫자 가독성 낮음 숫자지만 긴 숫자임
운영 난이도 낮음 높음
현재 구조 적합성 높음 조건부 적합

Snowflake가 동작하는 방식

Snowflake는 다음 정보를 조합해 64비트 숫자를 만듦.

시간 정보 + 서버(Worker) ID + 같은 시간 안의 순번

각 프로젝트 서버마다 서로 다른 Worker ID를 배정해야 함.

프로젝트 A 서버: Worker ID 1
프로젝트 B 서버: Worker ID 2
프로젝트 C 서버: Worker ID 3

서버가 늘어나거나 컨테이너가 추가될 때마다 번호가 겹치지 않도록 관리해야 함. Worker ID가 중복되면 ID 충돌이 발생할 수 있음. 서버 시간이 뒤로 이동하면 일부 구현에서는 ID 발급을 중단할 수도 있으므로 시간 동기화도 필요함.

ULID가 동작하는 방식

ULID는 현재 시간과 무작위 값을 조합함.

시간 정보 + 충돌 가능성이 매우 낮은 무작위 값

각 프로젝트 서버에 번호를 따로 배정할 필요가 없음. 여러 서버가 동시에 생성해도 충돌 가능성이 매우 낮고, PK 유일 제약으로 최종 중복도 차단할 수 있음.

6. 현재 구조에 맞는 3가지 케이스

Case 1. BIGINT PK + 프로젝트 코드 별도 표시

구성

DB ID:       10492
프로젝트:    ABC
채널:        WEB
화면 표시:   ABC-WEB-10492
  • DB: id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
  • 프로젝트와 채널은 별도 컬럼으로 저장
  • 화면과 알림에서 프로젝트 코드와 숫자를 조합

장점

  • 정수 PK와 외래키로 인덱스가 작음
  • 중앙 DB에서 구현하기 쉬움
  • 기존 구조를 가장 적게 변경함
  • Buffer Pool에 데이터와 인덱스를 상주시킬 수 있음

주의점

  • 작성 서버가 글을 전송하기 전에 중앙 DB의 ID를 알 수 없음
  • 네트워크 재시도 중복을 막으려면 별도의 Idempotency Key가 필요함
  • 화면의 ABC-WEB-10492와 DB의 10492가 달라질 수 있음

적합한 경우

  • 중앙 DB 저장 완료 후 ID를 받아도 되는 경우
  • 기존 시스템 변경을 최소화해야 하는 경우
  • 화면 ID와 실제 DB ID가 반드시 같을 필요가 없는 경우

Case 2. ULID 또는 UUID v7을 실제 글 ID로 사용

구성

화면 ID와 API ID를 동일하게 유지하려면 전체 식별자를 DB에 그대로 저장함.

DB ID / API ID / 화면 ID:
PRJA_01K8A9V4N5J9F28D5G3H1A2B3C

가능한 저장 방식은 두 가지임.

방식 설명
전체 문자열 저장 접두사와 ULID를 VARCHAR에 저장해 화면 ID와 DB ID를 동일하게 유지함
분리 저장 프로젝트 코드와 ULID를 컬럼으로 나누고, 화면에서 다시 조합함

이번 요구처럼 글 ID와 표시 ID를 일치시키려면 전체 문자열 저장이 더 명확함. 이 경우 문자열 PK의 인덱스 크기를 감수해야 함.

등록 흐름

1. 프로젝트 작성 서버가 ID를 먼저 생성함
2. 글 데이터와 ID를 중앙 API로 전송함
3. 중앙 DB가 ID를 PK 또는 UNIQUE KEY로 확인함
4. 같은 ID의 재요청이면 중복 저장하지 않음
5. 중앙 관리페이지는 전달받은 ID를 그대로 표시함

장점

  • 프로젝트별 작성 서버가 독립적으로 ID를 선발급할 수 있음
  • 타임아웃 후 재시도해도 같은 글로 처리할 수 있음
  • 중앙 번호 발급을 기다리지 않아 비동기 전송이 쉬움
  • 시간 정보가 앞에 있어 B-Tree 인덱스가 UUID v4보다 유리함
  • 별도의 Worker ID 관리가 필요 없음

단점

  • BIGINT보다 PK와 외래키 인덱스가 큼
  • 사람이 숫자 하나로 기억하거나 구두 전달하기 어려움
  • 프로젝트 접두사를 포함하면 ID가 더 길어짐

적합한 경우

  • 프로젝트별 작성 서버가 완전히 분리되어 있음
  • 중앙 API로 여러 서버의 데이터를 수집함
  • 글 ID와 화면 표시 ID를 일치시켜야 함
  • 재시도 중복과 비동기 전송을 안정적으로 처리해야 함

Case 3. Snowflake 숫자 ID + 프로젝트 정보 별도 저장

구성

DB ID:       178293849182394880
프로젝트:    ABC
화면 표시:   178293849182394880
  • id BIGINT UNSIGNED PRIMARY KEY
  • 프로젝트 코드는 별도 컬럼으로 저장
  • 각 작성 서버에 고유 Worker ID를 배정함
  • 모든 서버의 시계를 동기화함

장점

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

단점

  • 프로젝트와 서버별 Worker ID 관리가 필요함
  • 오토스케일링과 서버 추가 시 번호 배정 정책이 필요함
  • Worker ID 중복 여부를 관리해야 함
  • 시계가 뒤로 이동하는 상황에 대한 처리 필요
  • 현재 등록량에서는 운영 복잡도가 성능 이점보다 클 수 있음

적합한 경우

  • 숫자 ID가 반드시 필요함
  • 초당 수천~수만 건의 등록이 발생함
  • 서버와 Worker ID를 중앙에서 안정적으로 관리할 수 있음

7. 선택 결론

현재 구조에서는 Case 2: ULID 또는 UUID v7을 실제 글 ID로 사용하는 방식이 가장 자연스러움.

이유는 다음과 같음.

  1. 프로젝트별 작성 서버가 중앙 DB에 의존하지 않고 ID를 먼저 만들 수 있음
  2. 네트워크 타임아웃과 재시도에 따른 중복 등록을 막기 쉬움
  3. 프로젝트 서버가 늘어나도 Worker ID를 따로 배정할 필요가 없음
  4. 시간 순 ID라 UUID v4보다 인덱스 페이지 분할에 유리함
  5. 전체 ID를 DB에 그대로 저장하면 화면 ID와 API ID를 일치시킬 수 있음

다만 숫자 ID가 반드시 필요하다면 Case 3을 선택할 수 있음. 이 경우 ULID보다 저장 공간은 작지만 Worker ID와 서버 시계 관리가 필수임.

8. 일상적인 비유

Snowflake

각 지점에 번호표 기계를 설치하고 지점 번호를 미리 배정하는 방식임.

  • 강남점은 1번
  • 판교점은 2번
  • 여의도점은 3번

지점 번호가 겹치지 않으면 아주 작은 숫자 번호표를 빠르게 만들 수 있음. 하지만 지점이 늘어날 때마다 번호를 관리해야 하고, 지점 시계가 맞지 않으면 번호표 발급에 문제가 생길 수 있음.

ULID

각 지점이 별도 등록 없이 앱을 실행해 바로 고유 번호표를 만드는 방식임.

  • 지점 번호를 따로 배정하지 않아도 됨
  • 여러 지점에서 동시에 만들어도 충돌 가능성이 매우 낮음
  • 네트워크가 끊겨 같은 번호표를 다시 보내도 중앙에서 중복을 확인할 수 있음
  • 번호표가 시간 순으로 만들어져 정리하기 쉬움

현재처럼 프로젝트별 작성 서버가 여러 곳에 나뉘어 있다면, 관리해야 할 설정이 적은 ULID가 운영 측면에서 유리함.

9. 피드백 테이블 인덱스 설계

주요 조회 조건

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);

10. 5만 건 기준 메모리와 캐시

현재 523행의 데이터 용량이 약 528.0 KiB라면 다음과 같이 추정할 수 있음.

항목 예상 크기
5만 건 데이터 약 50~70MB
복합 인덱스 2~3개 약 4~6MB
데이터와 인덱스 합계 약 80MB 미만

실제 크기는 JSON 또는 TEXT 데이터 길이, 인덱스 자료형, 보조 인덱스 수에 따라 달라짐.

InnoDB Buffer Pool

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

Buffer Pool이 데이터와 인덱스보다 충분히 크면 자주 조회되는 페이지가 메모리에 상주할 수 있음. 다만 “항상 100% 상주” 또는 “항상 1ms 이하”라고 단정할 수는 없으며, 서버 메모리와 실행 계획을 확인해야 함.

Redis 애플리케이션 캐시

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

우선 캐싱할 대상:

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

캐시 키 예시:

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

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

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

11. 성능 점검 방법

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

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