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

14 KiB

게시판 글 ID 및 피드백 성능 설계

게시판 글 ID(Primary Key 및 식별자)는 조회 성능, 보안, 분산 환경 지원 여부, URL 가독성 등에 따라 여러 방식으로 설계할 수 있음.

1. 자동 증가 정수형

Auto Increment / Sequence

MySQL의 AUTO_INCREMENT, PostgreSQL의 BIGSERIAL 또는 IDENTITY를 사용하는 전통적인 방식.

구분 내용
형태 1, 2, 3, 4, ...
저장 방식 INT 또는 BIGINT
구현 난이도 가장 낮음
인덱스 성능 매우 좋음

장점

  • 구현이 가장 쉽고 직관적임
  • 저장 용량이 적음
  • B-Tree 인덱스 정렬 성능이 좋음
  • 최신 글 순서대로 자동 정렬하기 쉬움

단점

  • 다음 글 번호를 쉽게 추측할 수 있음
  • 크롤링이나 글 수량 파악에 취약할 수 있음
  • 분산 DB 또는 멀티 마스터 환경에서 충돌 없이 발급하기 어려움

적합한 사용처

  • 사내 인트라넷
  • 관리자 전용 게시판
  • 단일 DB 인스턴스 환경

2. UUID v4

완전 랜덤 128비트 식별자

전 세계에서 고유한 값을 무작위로 생성하는 표준 방식.

구분 내용
형태 c9a646d3-9c61-4cc9-bc19-482386a37365
저장 방식 UUID 또는 CHAR(36)
생성 방식 완전 랜덤
분산 환경 지원

장점

  • 중복 가능성이 매우 낮음
  • 중앙 조율 없이 생성 가능함
  • 다음 ID를 추측하기 어려움
  • 분산 서버 환경에 적합함

단점

  • 36자 문자열이라 URL이 길고 복잡함
  • 완전 랜덤 값이라 B-Tree 인덱스 페이지 분할이 자주 발생할 수 있음
  • 대량 입력 환경에서 정수형보다 쓰기 성능이 낮을 수 있음

적합한 사용처

  • 외부 API 통신 식별자
  • 추측 방지가 중요한 비즈니스 데이터
  • 분산 시스템

3. 시간 순 정렬 가능 고유 식별자

UUID v7 / ULID

UUID의 고유성과 Auto Increment의 시간 정렬 장점을 결합한 방식.

방식 예시
UUID v7 018d3b2f-7a54-7c3a-8b1e-9d2a4e5b6c7d
ULID 01ARZ3NDEKTSV4RRFFQ69G5FAV

장점

  • 생성된 시간 순으로 정렬 가능함
  • B-Tree 인덱스에 순차적으로 적재되어 쓰기 성능이 좋음
  • 분산 환경에서 충돌 없이 생성 가능함
  • 다음 ID를 추측하기 어려움

단점

  • 정수형보다 저장 공간을 많이 사용함
  • UUID v4보다 생성 규칙이 복잡함
  • ID만 보고 생성 시점을 일부 추정할 수 있음

적합한 사용처

  • 대용량 트래픽 게시판
  • 분산 서비스
  • 최신 웹 애플리케이션

4. 트위터 스노우플레이크

Snowflake

대량의 데이터를 분산 환경에서 생성하기 위해 고안된 64비트 정수 기반 방식.

기본 구조

타임스탬프 41비트 + 데이터센터/머신 ID 10비트 + 시퀀스 번호 12비트

예시

1542893478912349184

장점

  • 64비트 정수라 인덱싱 성능이 좋음
  • 시간 순 정렬이 가능함
  • 분산 서버 간 ID 충돌을 방지할 수 있음
  • 대규모 시스템에서 높은 처리량을 지원함

단점

  • 별도의 ID 생성기 또는 Worker 관리가 필요함
  • 데이터센터 및 머신 ID 관리가 필요함
  • JavaScript에서 일반 숫자로 처리하면 정밀도 손실이 발생할 수 있음
  • Auto Increment보다 운영 복잡도가 높음

적합한 사용처

  • 대규모 분산 SNS
  • 초대형 커뮤니티
  • 초당 수천 건 이상의 글이 등록되는 서비스

5. 해시 기반 난독화

Sqids / Hashids

DB에는 정수형 ID를 저장하고, URL이나 외부 화면에서는 별도의 문자열로 변환하여 표시하는 방식.

구분 예시
내부 DB ID 1204
외부 URL ID b9Xq

장점

  • DB에서는 정수형 인덱스 성능을 유지할 수 있음
  • 짧고 깔끔한 URL을 만들 수 있음
  • 외부에서 원래 순번을 바로 유추하기 어려움
  • 기존 정수형 DB 구조를 유지하면서 외부 노출만 변경할 수 있음

단점

  • 내부 ID와 외부 표시 ID가 달라짐
  • 암호화가 아니라 인코딩에 가까움
  • 시크릿 솔트가 노출되면 원래 정수값을 역산할 수 있음
  • 운영자가 같은 글을 서로 다른 ID로 인식할 수 있음

적합한 사용처

  • 외부 URL을 짧게 만들고 싶은 서비스
  • 기존 정수형 DB를 유지하면서 외부 노출만 바꾸려는 서비스
  • 짧은 URL 식별자가 필요한 서비스

6. 비즈니스 접두사 결합형

Stripe 스타일 ID

글의 종류나 도메인을 식별할 수 있도록 ID 앞에 접두사를 붙이는 방식.

예시

post_01ARZ3NDEKTSV4RRFFQ69G5FAV
qna_01ARZ3NDEKTSV4RRFFQ69G5FAV
fb_202609_a8f3d

장점

  • ID만 보고도 데이터 종류를 구분할 수 있음
  • 로그나 알림에서 식별이 쉬움
  • API 중심 서비스에 적합함
  • 디버깅과 모니터링이 편리함

단점

  • 숫자만 사용하는 방식보다 ID가 길어짐
  • 글 종류가 변경되면 접두사 정책도 고려해야 함
  • 단순한 게시판에서는 다소 복잡할 수 있음

적합한 사용처

  • API 중심 B2B SaaS
  • 여러 유형의 게시물이 함께 존재하는 시스템
  • 로그와 웹훅에서 데이터 유형을 즉시 구분해야 하는 서비스

방식별 비교

방식 저장 타입 URL 길이 분산 환경 인덱스 성능 추측 방지 적합한 상황
Auto Increment INT / BIGINT 매우 짧음 낮음 매우 좋음 낮음 내부 관리용, 소규모 서비스
UUID v4 UUID / CHAR 높음 보통 높음 보안이 중요한 데이터
UUID v7 / ULID UUID / 16바이트 등 중간 높음 좋음 높음 대용량·분산 웹 서비스
Snowflake BIGINT 짧음 높음 매우 좋음 보통 대규모 분산 시스템
Sqids / Hashids DB는 정수형 외부는 짧음 낮음 매우 좋음 보통 URL 난독화가 필요한 경우
접두사 결합형 문자열 또는 조합형 중간~김 방식에 따라 다름 방식에 따라 다름 방식에 따라 다름 API·로그 식별이 중요한 경우

검토할 때 확인할 기준

ID 방식을 선택할 때는 다음 항목을 함께 확인해야 함.

  1. 화면에 표시되는 ID와 DB의 실제 ID를 같게 유지할 것인지
  2. 프로젝트마다 번호를 새로 시작할 것인지
  3. 프로젝트를 넘어선 전체 글 번호가 필요한지
  4. 피드백과 이슈가 같은 번호 체계를 사용할 것인지
  5. 외부 Q&A 원본 ID와 관리페이지 ID를 어떻게 연결할 것인지
  6. URL에서 ID를 직접 노출할 것인지
  7. 단일 서버인지 분산 서버인지
  8. 향후 글 수가 얼마나 증가할지

특히 내부 DB ID와 화면 표시 ID를 별도로 운영하면, 같은 글을 서로 다른 번호로 인식할 수 있으므로 표시 정책을 먼저 확정해야 함.


피드백 테이블 성능 및 캐시 설계

1. 현재 테이블 구조

기본 구성

  • 데이터베이스 엔진: MySQL 8.0.46
  • 스토리지 엔진: InnoDB
  • 기본 컬럼
    • id: 기본 키
    • created_at: 등록 일시
    • updated_at: 수정 일시
    • deleted_at: 소프트 삭제 일시
    • channel_id: 채널 ID
    • data: 제목, 내용, 카테고리 등을 저장하는 JSON 또는 TEXT 컬럼
    • admin_first_read_at: 관리자가 처음 확인한 일시
    • admin_first_read_by: 처음 확인한 관리자

주요 조회 패턴

  1. 특정 채널의 삭제되지 않은 피드백을 최신순으로 조회
  2. 특정 채널에서 아직 확인하지 않은 피드백만 조회
  3. data 내부의 category 값으로 피드백 필터링
  4. 목록을 페이지 단위로 조회

2. 인덱스 설계

2.1 채널별 피드백 목록 조회

목록 조회는 다음과 같은 조건과 정렬을 사용함.

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

channel_id, deleted_at, id를 하나의 복합 인덱스로 구성하면 채널과 활성 상태로 범위를 좁힌 뒤 최신순으로 바로 조회할 수 있음.

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

idAUTO_INCREMENT라면 created_at 대신 id를 정렬 기준으로 사용할 수 있음. 일반적으로 ID 증가 순서와 등록 순서가 일치하므로 정렬 비용을 줄이기 쉬움.

2.2 미확인 피드백 조회

관리자가 아직 확인하지 않은 피드백을 자주 조회한다면 다음 인덱스를 사용할 수 있음.

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

이 인덱스는 특정 채널에서 admin_first_read_at IS NULL 조건을 사용하는 조회의 전체 테이블 스캔을 줄이는 데 도움을 줌.

2.3 JSON 내부 값 조회

category처럼 data 내부의 값을 조건으로 자주 조회한다면 생성 컬럼을 추가해 인덱스를 구성할 수 있음.

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

생성 컬럼을 VIRTUAL로 만들면 별도의 값을 테이블에 저장하지 않고 조회 시 계산할 수 있음. 해당 값의 검색 빈도와 데이터 분포를 확인한 뒤 적용해야 함.

3. 5만 건 기준 데이터 및 메모리 규모

현재 523행의 데이터 용량이 약 528.0 KiB라면 행 하나의 평균 크기는 약 1 KiB 수준임.

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

실제 크기는 JSON 또는 TEXT 데이터의 길이, 인덱스 컬럼의 자료형, 인덱스 페이지 여유 공간에 따라 달라질 수 있음.

InnoDB Buffer Pool이 데이터와 인덱스보다 충분히 크다면 자주 조회되는 데이터가 메모리에 상주할 수 있음. 이 경우 반복 조회에서 디스크 I/O가 줄어들고 인덱스 기반 조회의 응답 시간이 짧아짐.

다만 실제 성능은 다음 조건에 따라 달라짐.

  • 서버의 가용 메모리
  • innodb_buffer_pool_size 설정
  • 동시 접속자 수
  • 쿼리의 실제 실행 계획
  • JSON 또는 TEXT 컬럼의 평균 크기
  • 페이지 번호가 뒤로 갈수록 커지는 OFFSET 사용 여부

4. 캐시 구성

캐시는 MySQL 내부 캐시와 애플리케이션 외부 캐시로 나눌 수 있음.

[클라이언트 요청]
        │
        ▼
┌────────────────────────┐
│ Redis 애플리케이션 캐시 │
└──────────┬─────────────┘
           │
     HIT ──┤  캐시된 목록 즉시 반환
           │
    MISS  ▼
┌─────────────────────────┐
│ MySQL InnoDB Buffer Pool │
└──────────┬──────────────┘
           │
           ▼
     DB 조회 후 Redis 갱신

4.1 InnoDB Buffer Pool

InnoDB Buffer Pool은 테이블 데이터와 인덱스 페이지를 메모리에 보관함.

  • 반복 조회 시 디스크 접근을 줄일 수 있음
  • 복합 인덱스가 있으면 필요한 범위만 빠르게 탐색할 수 있음
  • 데이터와 인덱스 전체 크기보다 충분히 큰 Buffer Pool이 필요함
  • Buffer Pool 크기는 서버의 다른 프로세스가 사용할 메모리를 고려해 설정해야 함

4.2 Redis 애플리케이션 캐시

동시 접속자가 많거나 동일한 목록을 반복 조회하는 경우 Redis를 추가할 수 있음.

우선 캐싱할 대상

  • 채널별 최신 피드백 목록
  • 채널별 첫 페이지 목록
  • 전체 피드백 수 또는 상태별 개수
  • 답변 대기, 미확인 등 자주 표시되는 요약 정보

캐시 키 예시

feedback:channel:{channel_id}:page:1

실제 키에는 프로젝트, 채널, 페이지 크기, 정렬 조건, 검색 조건 등 결과에 영향을 주는 값을 포함해야 함.

캐시 만료 및 무효화

새 피드백이 등록되거나 기존 피드백이 수정·삭제되면 해당 채널의 목록 캐시를 갱신해야 함.

방식 설명
즉시 삭제 등록·수정·삭제 시 관련 캐시를 바로 삭제함
짧은 TTL 캐시 만료 시간을 30초~1분 정도로 설정함
조합 중요 목록은 즉시 삭제하고 나머지는 짧은 TTL을 사용함

첫 페이지는 새 글 등록의 영향을 가장 많이 받으므로 우선적으로 삭제하거나 갱신해야 함.

5. 조회 성능 점검 방법

인덱스를 추가하기 전에 실제 쿼리의 실행 계획을 확인해야 함.

EXPLAIN ANALYZE
SELECT *
FROM feedbacks
WHERE channel_id = 1
  AND deleted_at IS NULL
ORDER BY id DESC
LIMIT 20;

다음 항목을 확인함.

  • 사용할 인덱스가 예상대로 선택되는지
  • type이 불필요한 전체 테이블 스캔인지
  • key에 복합 인덱스가 표시되는지
  • rows가 과도하게 많지 않은지
  • ExtraUsing filesort 또는 Using temporary가 발생하는지

6. 적용 시 주의사항

  • 인덱스는 조회 속도를 높이지만 등록·수정·삭제 시 추가 비용이 발생함
  • 사용하지 않는 인덱스는 저장 공간과 쓰기 성능을 낭비함
  • deleted_at의 값 분포가 대부분 NULL이면 인덱스 효과가 제한될 수 있음
  • JSON 필드는 자주 검색하는 값만 생성 컬럼으로 분리하는 것이 좋음
  • OFFSET이 큰 페이지에서는 커서 기반 조회를 검토할 수 있음
  • 인덱스 적용 전후에 실제 운영 쿼리의 실행 계획과 응답 시간을 비교해야 함