Elasticsearch 마스터 노트 시리즈 9편. 실전 검색 엔진 프로젝트 설계, DB와 ES의 역할 분담·동기화 전략(application·CDC·Outbox), Index 명명·Alias 패턴, ILM (Index Lifecycle Management), Shard 수 결정 가이드, 무중단 Reindex 워크플로우, 검색 품질 모니터링까지.
이 글은 Elasticsearch 마스터 노트 시리즈의 아홉 번째 편입니다. 1~8편이 ES 자체였다면, 이번엔 실전 시스템 설계 — 검색 엔진 프로젝트.
DB가 단일 진실, ES가 검색. 그 사이 동기화가 핵심. Index·Alias·ILM 패턴으로 운영. 무중단 Reindex 표준.
처음 시스템 설계가 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, DB와 ES 동기화 방법이 막연합니다. 둘째, Reindex 무중단이 어떻게 가능한지 알기 어렵습니다.
해결법은 한 가지예요. "Alias로 인덱스 이름 가리기" + "DB가 단일 진실" 두 줄. Alias 전환 = 무중단. DB 변경 → ES 동기. 이 두 원칙이 시스템 설계 토대.
검색 엔진 아키텍처
[Client]
↓
[API Server (Spring Boot)]
├── Read → ES (검색)
└── Write → DB (운영 데이터)
↓ 동기화
ES (인덱싱)
DB = 단일 진실, ES = 검색 최적화 사본.
DB ↔ ES 동기화 전략
1. Application-Level Dual Write
@Transactional
public Product save(Product p) {
Product saved = dbRepo.save(p);
esRepo.save(saved); # 같은 메서드에서 둘 다
return saved;
}
장점 — 단순. 단점:
- DB 트랜잭션과 ES 분리 (ES 실패 = 일관성 깨짐)
- 부분 실패 처리 어려움
2. CDC (Change Data Capture) — Debezium
PostgreSQL → Debezium (CDC) → Kafka → ES Sink Connector → ES
DB 트랜잭션 로그 감지 → Kafka → ES.
장점:
- DB 보장 = ES 보장
- 비동기·확장
- 모든 변경 자동 (앱 코드 변경 X)
단점 — 복잡·운영 부담.
3. Transactional Outbox + Kafka
Kafka 시리즈 7편 패턴:
@Transactional
public void save(Product p) {
productRepo.save(p);
outboxRepo.save(new Outbox("product-update", toJson(p)));
}
# 폴러 (별도 프로세스)
List<Outbox> events = outboxRepo.fetchPending();
for (Outbox e : events) {
kafkaProducer.send("product-events", e);
outboxRepo.delete(e);
}
# Kafka Consumer
@KafkaListener(topics = "product-events")
public void onProduct(ProductEvent event) {
esRepo.save(event.toProduct());
}
여기서 정말 중요한 시험 함정 — 운영 표준 = Outbox 또는 CDC. 단순 Dual Write는 일관성 위험. 큰 시스템 = CDC, 중간 = Outbox.
Index 명명 규칙
단일 인덱스
products # 단순
시간 기반
logs-2024-01
logs-2024-02
logs-2024-03
로그·시계열에. ILM과 결합.
버전·환경
products-v1
products-v2
products-prod
products-dev
Reindex 마이그레이션·환경 분리.
Alias — 인덱스 이름 가리기
# Alias 생성
POST /_aliases
{
"actions": [
{ "add": { "index": "products-v1", "alias": "products" } }
]
}
클라이언트는 항상 products (alias) 사용:
@Document(indexName = "products")
public class Product { ... }
여기서 정말 중요한 시험 함정 — 운영 = 모든 인덱스에 Alias. Reindex·재배치 시 무중단. 직접 인덱스 이름 사용 X.
다중 인덱스 Alias
POST /_aliases
{
"actions": [
{ "add": { "index": "logs-2024-01", "alias": "logs-current" } },
{ "add": { "index": "logs-2024-02", "alias": "logs-current" } }
]
}
여러 인덱스 → 하나의 alias. 시계열에.
Filter Alias
POST /_aliases
{
"actions": [
{
"add": {
"index": "products",
"alias": "active-products",
"filter": { "term": { "status": "active" } }
}
}
]
}
Alias가 자동 필터링. 권한·뷰 분리.
Write Alias vs Read Alias
# Write Alias — 한 인덱스만
{ "add": { "index": "products-v2", "alias": "products-write", "is_write_index": true } }
# Read Alias — 여러 인덱스
{ "add": { "index": "products-v1", "alias": "products-read" } }
{ "add": { "index": "products-v2", "alias": "products-read" } }
Reindex 중 — write는 v2만, read는 v1+v2 (점진 전환).
무중단 Reindex 워크플로우
1. 새 인덱스 생성 (products-v2, 새 매핑)
2. 기존 데이터 Reindex (products-v1 → products-v2)
3. 동기화 시작 시점 기록 (timestamp)
4. 그 시점 이후 변경 사항 적용
- Application: dual write
- CDC: 자동
5. Alias 전환 — products → products-v2
6. 검증 — 검색 결과 비교
7. products-v1 삭제
여기서 시험 함정이 하나 있어요. Reindex 중 신규 데이터 처리 = 가장 어려운 부분. write alias 또는 dual write로 처리. 또는 CDC가 자동.
ILM — Index Lifecycle Management
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_size": "50GB", "max_age": "7d" }
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"cold": {
"min_age": "30d",
"actions": { "freeze": {} }
},
"delete": {
"min_age": "365d",
"actions": { "delete": {} }
}
}
}
}
4 단계 — Hot (최신·자주 쓰임) / Warm (덜) / Cold (드물게) / Delete.
자동 전환·관리. 시계열·로그에 표준.
Shard 수 결정
Shard 크기 권장: 30~50 GB
총 데이터: 1 TB → Shard 25개 정도
일별 증가: 10 GB·30 GB Shard → 3일치 1 Shard
여기서 정말 중요한 시험 함정 — 너무 적은 Shard = 단일 노드 부담 / 너무 많은 = 메타데이터 폭주. 30~50 GB per shard 권장. 작은 인덱스 (수 GB) = Shard 1·Replica 1 충분.
Replica 수 결정
Replica 0: HA X·인덱싱 빠름 (초기 로드만)
Replica 1: 표준 (1 노드 다운까지 OK)
Replica 2+: 더 안전·읽기 분산 ↑
Read 위주 = Replica ↑·Read 분산.
검색 성능 튜닝
# refresh interval (기본 1초)
PUT /products/_settings
{
"index": {
"refresh_interval": "30s" # 30초마다 = 검색 가능
}
}
# search_idle 시 자동 단축
"search.idle.after": "30s"
여기서 시험 함정이 하나 있어요. refresh_interval ↑ = 인덱싱 빠름·검색 지연 ↑. 운영 — 30초가 일반. 즉시 검색 필요 = 1초.
Coordinating Node
큰 클러스터:
Coordinating Node (검색 라우팅·집계 결합) 분리
Data Node 부하 ↓
검색 품질 모니터링
메트릭
- Query Latency (p50·p99)
- Query Rate (QPS)
- Failed Queries
- Slow Queries (>1s)
- Cache Hit Rate
Slow Log
PUT /products/_settings
{
"index.search.slowlog.threshold.query.warn": "1s",
"index.search.slowlog.threshold.query.info": "500ms"
}
느린 쿼리 자동 로그.
사용자 메트릭
- 검색 후 클릭률 (CTR)
- 빈 검색 결과 비율
- "Did you mean?" 클릭률
- 첫 결과 클릭 비율
검색 품질 직접 측정. A/B 테스트.
캐시 전략
# Field Data Cache (집계용)
indices.fielddata.cache.size: 30%
# Query Cache (필터 캐시)
indices.queries.cache.size: 10%
# Request Cache (자주 쓰는 검색)
indices.requests.cache.size: 1%
Filter Context = Query Cache 자동.
백업·복구
Snapshot
# Repository 등록 (S3·로컬·HDFS)
PUT /_snapshot/my_backup
{
"type": "s3",
"settings": {
"bucket": "my-es-backup",
"region": "us-east-1"
}
}
# Snapshot 생성
PUT /_snapshot/my_backup/snapshot_2024-01-15
{
"indices": "products,users",
"include_global_state": false
}
# 복구
POST /_snapshot/my_backup/snapshot_2024-01-15/_restore
여기서 정말 중요한 시험 함정 — Snapshot은 운영 필수. 정기 백업 (일·시간)·다른 리전 보관. 재해 복구.
운영 체크리스트
✓ DB가 단일 진실
✓ ES = 검색·분석 사본
✓ 동기화 — Outbox 또는 CDC
✓ 모든 인덱스에 Alias
✓ 명시 매핑 (Dynamic 회피)
✓ Shard 30~50 GB 권장
✓ Replica 1 (운영 표준)
✓ ILM (시계열·로그)
✓ refresh_interval 30s (운영)
✓ Slow Log 활성
✓ 정기 Snapshot (S3 등)
✓ Master 3+ (정족수)
✓ 모니터링 — Prometheus·Kibana
✓ 검색 품질 — CTR·빈 결과율
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 9편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 아키텍처 — DB (단일 진실) + ES (검색)
- 동기화 — Application·CDC (Debezium)·Outbox + Kafka
- 운영 = Outbox 또는 CDC (단순 Dual Write 위험)
- Index 명명 — 단일·시간 기반·버전 (v1, v2)
- 모든 인덱스에 Alias 권장
- Alias = 무중단 교체
- 다중 인덱스 Alias·Filter Alias·Write·Read Alias
- 무중단 Reindex — 새 인덱스 + Reindex + dual write·CDC + Alias 전환
- ILM = 시계열·로그 자동 관리
- 4 단계 — Hot / Warm / Cold / Delete
- Shard 권장 = 30~50 GB
- 작은 인덱스 = 1 Shard 충분
- Replica 1 = 운영 표준
refresh_interval30s = 운영 (1s = 즉시 검색)- Coordinating Node 분리 (큰 클러스터)
- Slow Log 활성
- 검색 품질 — CTR·빈 결과율·"Did you mean?"
- A/B 테스트
- Snapshot 정기 백업 (S3 등)
- 다른 리전 보관 (재해 복구)
- 운영 체크 — DB 단일 진실·동기화·Alias·매핑·Shard·Replica·ILM·refresh·Slow Log·Snapshot·Master 3+·모니터링·검색 품질
시리즈 다른 편
- 1편 — 기본 개념·Cluster·Shard
- 2편 — Mapping·데이터 타입
- 3편 — Analyzer·Tokenizer·한국어
- 4편 — Query DSL
- 5편 — Full-text Search·Relevance
- 6편 — Aggregations
- 7편 — Bulk API·Reindex
- 8편 — Spring Data Elasticsearch
- 9편 — 검색 엔진 프로젝트 설계 (현재 글)
- 10편 — Security·인증·인가·TLS
공식 문서: Index Lifecycle Management / Snapshot and Restore 에서 더 깊이.
다음 글(10편, 마지막)에서는 Security — 인증·인가·TLS·X-Pack·Audit Log까지 시리즈 마무리.