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 방식을 선택할 때는 다음 항목을 함께 확인해야 함.
- 화면에 표시되는 ID와 DB의 실제 ID를 같게 유지할 것인지
- 프로젝트마다 번호를 새로 시작할 것인지
- 프로젝트를 넘어선 전체 글 번호가 필요한지
- 피드백과 이슈가 같은 번호 체계를 사용할 것인지
- 외부 Q&A 원본 ID와 관리페이지 ID를 어떻게 연결할 것인지
- URL에서 ID를 직접 노출할 것인지
- 단일 서버인지 분산 서버인지
- 향후 글 수가 얼마나 증가할지
특히 내부 DB ID와 화면 표시 ID를 별도로 운영하면, 같은 글을 서로 다른 번호로 인식할 수 있으므로 표시 정책을 먼저 확정해야 함.
피드백 테이블 성능 및 캐시 설계
1. 현재 테이블 구조
기본 구성
- 데이터베이스 엔진: MySQL 8.0.46
- 스토리지 엔진: InnoDB
- 기본 컬럼
id: 기본 키created_at: 등록 일시updated_at: 수정 일시deleted_at: 소프트 삭제 일시channel_id: 채널 IDdata: 제목, 내용, 카테고리 등을 저장하는 JSON 또는 TEXT 컬럼admin_first_read_at: 관리자가 처음 확인한 일시admin_first_read_by: 처음 확인한 관리자
주요 조회 패턴
- 특정 채널의 삭제되지 않은 피드백을 최신순으로 조회
- 특정 채널에서 아직 확인하지 않은 피드백만 조회
data내부의category값으로 피드백 필터링- 목록을 페이지 단위로 조회
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);
id가 AUTO_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가 과도하게 많지 않은지Extra에Using filesort또는Using temporary가 발생하는지
6. 적용 시 주의사항
- 인덱스는 조회 속도를 높이지만 등록·수정·삭제 시 추가 비용이 발생함
- 사용하지 않는 인덱스는 저장 공간과 쓰기 성능을 낭비함
deleted_at의 값 분포가 대부분NULL이면 인덱스 효과가 제한될 수 있음- JSON 필드는 자주 검색하는 값만 생성 컬럼으로 분리하는 것이 좋음
OFFSET이 큰 페이지에서는 커서 기반 조회를 검토할 수 있음- 인덱스 적용 전후에 실제 운영 쿼리의 실행 계획과 응답 시간을 비교해야 함