Redis 캐싱과 Elasticsearch 검색·분석 아키텍처

Redis 캐시와 Elasticsearch 검색·분석을 함께 운영할 때의 일관성, 확장, 장애 복구, 용량 산정 기준을 정리한다.

2026-08-14 · 최초 발행 2024-04-29

RDBMS만으로 증가한 읽기 요청과 실시간 검색·분석 요구를 모두 처리하면, 데이터베이스 부하와 응답 지연이 함께 커질 수 있다. Redis는 자주 읽는 데이터를 메모리에 두어 읽기 경로를 가볍게 만들고, Elasticsearch는 역색인과 집계 기능으로 검색·로그·행동 데이터를 다루는 역할을 맡는다.

Redis는 인메모리 데이터 구조 서버다. 낮은 지연의 키-값 저장소로 사용할 수 있고 TTL 기반 캐시, Pub/Sub, 분산 락, 카운터 같은 실시간 처리 기능도 제공한다. Elasticsearch는 분산 검색·분석 엔진으로, 역색인 기반 전문 검색과 aggregations, 스키마 매핑, 샤드·레플리카를 통한 수평 확장을 지원한다.

두 시스템을 조합하면 Redis는 읽기 경로의 지연과 DB 부하를 줄이고, Elasticsearch는 검색과 집계 처리를 분리하는 기반이 된다.

캐시와 인덱스를 각각의 경로에 배치한다

캐시는 보통 Cache-aside 패턴으로 구성한다. 애플리케이션이 먼저 캐시를 조회하고, 미스가 나면 원장 저장소에서 데이터를 읽어 캐시에 넣는다. 이때 TTL만으로 끝내지 않고 키 무효화 정책을 함께 둬야 변경된 데이터가 오래 남는 상황을 줄일 수 있다.

쓰기 갱신 방식은 목적에 따라 달라진다. write-through는 쓰기 경로가 단순한 대신 지연이 늘 수 있고, write-behind는 지연 쓰기를 택하는 대신 복구가 복잡해진다. 변경 이벤트를 Pub/Sub 채널로 전달해 캐시를 무효화하는 방식도 사용할 수 있다.

검색과 분석 데이터는 추출, 정제, 매핑, 인덱싱으로 이어지는 ingest pipeline에서 다룬다. 실시간 검색에서는 refresh interval을 조정해 NRT 지연을 관리한다. 대량 적재에서는 Bulk API와 배치 크기, 동시성, 압축을 조정하고 실패 재시도와 DLQ를 구성한다.

Redis는 메모리와 CPU를 늘리는 수직 확장에 더해 Redis Cluster의 키 해시 슬롯 기반 확장과 일관 해시 기반 샤딩을 사용할 수 있다. Elasticsearch는 인덱스별 primary shards와 replicas를 조절하며, ingest·data·master 노드를 분리해 확장성과 안정성을 확보한다.

일관성과 복구 기준을 저장소별로 나눈다

Redis의 단일 스레드 명령은 원자성을 가지며 Lua 스크립트, RDB/AOF 지속성, Sentinel과 Cluster 기반 장애 조치를 활용할 수 있다. 다만 분산 락, 특히 Redlock은 주의할 지점이 있다.

Elasticsearch는 기본적으로 최종 일관성을 전제로 한다. replica ack를 acks=all로 둘지 선택하고, translog와 fsync, 스냅샷을 포함한 복구 전략을 마련해야 한다.

운영 시에는 Redis의 hit ratio, evicted_keys, cmdstat와 Elasticsearch의 indexing/search latency, queue size, GC/heap을 관찰한다. Redis Slowlog와 Elasticsearch slow search/index logs를 활성화하고, Dashboards와 경보 임계치로 이상 징후를 관리한다.

서비스에서 자주 만나는 적용 방식

전자상거래에서는 상품 상세를 캐시하고, 핫 아이템에는 TTL 5~15분을 적용할 수 있다. 재고나 가격이 바뀌면 캐시를 무효화하고, 카탈로그 검색·속성 필터·추천 결과 ranking은 Elasticsearch에서 처리한다.

세션과 토큰, 레이트 리미팅에는 Redis HyperLogLog, Bitmap, Counter를 사용할 수 있다. 분당 호출 제한이나 토큰 블랙리스트도 이 흐름에 포함된다.

애플리케이션 로그와 NGFW 로그는 Elasticsearch에 수집한 뒤 KQL/DSL 탐지 질의와 시간 버킷 집계에 활용할 수 있다. 좌표 기반 반경 검색인 geo_distance, 타일 집계, 주변 상점 추천 역시 검색 인덱스가 맡을 수 있는 작업이다.

캐시 히트 시 평균 응답 시간은 120ms에서 20ms 수준으로 단축 가능하며, p95는 2~5배 개선을 기대할 수 있다. 히트율 85%를 가정하면 읽기 트래픽은 1/6 수준으로 감소한다. 총 QPS가 6k이고 히트율이 0.85일 때 DB 읽기 QPS는 6k×(1−0.85)=900이며, 기존 6k 대비 85% 감소한다.

ES 단일 인덱스 5 shards, 3 data nodes 환경에서는 초당 수백~수천 문서 인덱싱과 검색 p95<200ms를 목표로 둘 수 있다. 다만 쿼리 복잡도와 데이터 분포에 의존하므로 환경별 벤치마크가 필요하다. 이런 분리는 DB 스케일업을 늦추고 라이선스·스토리지 비용을 완화하며, 장애 격리와 롤 분리로 운영 리스크를 줄일 수 있다. DSL, 집계, 자동완성, 하이라이트 같은 기능을 재사용하는 측면에서도 개발 생산성에 도움이 된다.

요청과 변경 데이터가 흐르는 경로

Elasticsearch수집·인덱싱 파이프라인원장 DB/서비스Redis Cluster애플리케이션클라이언트Cache HitCache MissCDCInvalidations사용자/서비스요청 처리캐시 미스 처리검색/집계 요청Key-ValuePub/SubInvalidationRDBMS/OLTPOutbox/Change LogETL/LogstashIndex/ShardsQuery/Aggregations

상품 조회 요청은 GET /product/{id}로 들어온다. 애플리케이션은 Redis GET을 먼저 시도하고, 미스면 DB를 조회한 뒤 TTL과 함께 Redis에 저장해 응답한다. JSON 응답에는 필요에 따라 캐시 히트 여부를 헤더로 기록할 수 있다.

캐시 히트 뒤 TTL을 갱신해야 한다면 touchEX를 사용한다. 미스가 연속으로 발생하는 상황에는 SET NX PX 기반 per-key 락으로 스파이크를 억제해 thundering herd를 완화한다. Redis에 오류가 나면 캐시를 건너뛰고 DB를 계속 사용하며, DB 오류에는 서킷브레이커와 핵심 필드만 반환하는 디그레이드가 필요하다.

Redis의 단일 키 원자 연산은 카운터와 락 구현에 쓸 수 있다. 다중 키는 Lua 스크립트나 분산 락을 고려한다. Redlock은 클록 드리프트와 네트워크 파티션에서 안전성 이슈가 있으므로, 임계 구간이 짧은 비즈니스에 한정해 사용한다.

검색 인덱스는 최종 일관성을 따른다. 변경 뒤 검색 반영 지연은 refresh interval(기본 1s) 또는 강제 refresh로 조절할 수 있지만, 강제 refresh는 쓰기 TPS 저하라는 트레이드오프가 있다.

Redis Cache-aside 구현 예시

전제조건은 Python 3.10+, redis 5.x, elasticsearch 8.x다. Redis 7.x 비SSL 예시이며, Elasticsearch 8.x에서 보안 기본 활성화 환경은 ID/Password/TLS 설정이 필요하다. 가상환경 구성과 네트워크·보안 그룹 허용 설정도 권장한다.

# pip install redis==5.* orjson==3.*
import time
import orjson
from redis import Redis
from typing import Optional

redis = Redis(host="localhost", port=6379, db=0, socket_timeout=0.1, socket_connect_timeout=0.1)

TTL_SEC = 600  # 10분

def serialize(obj) -> bytes:
    return orjson.dumps(obj)

def deserialize(b: bytes):
    return orjson.loads(b)

def get_product_from_db(pid: str) -> dict:
    # 실제 구현: RDBMS 조회
    return {"id": pid, "name": "Sample", "price": 1000, "stock": 5, "ts": int(time.time())}

def cache_key(pid: str) -> str:
    return f"product:{pid}:v1"

def get_product(pid: str) -> dict:
    k = cache_key(pid)
    try:
        v = redis.get(k)
        if v:
            return deserialize(v)
        # thundering herd 완화: per-key 락(최대 200ms)
        lock_key = f"lock:{k}"
        have_lock = redis.set(lock_key, "1", nx=True, px=200)
        if not v and not have_lock:
            # 잠깐 대기 후 재시도
            time.sleep(0.05)
            v2 = redis.get(k)
            if v2:
                return deserialize(v2)

        data = get_product_from_db(pid)
        # 캐시에 저장(부하 보호: NX로만 저장해 중복 방지 가능)
        redis.set(k, serialize(data), ex=TTL_SEC)
        return data
    except Exception:
        # 캐시 장애 시 폴백
        return get_product_from_db(pid)
    finally:
        try:
            redis.delete(f"lock:{k}")
        except Exception:
            pass

if __name__ == "__main__":
    print(get_product("A100"))

socket_timeout으로 빠른 타임아웃을 두고, per-key 락으로 캐시 스탬피드를 억제한다. TTL과 키 버전은 스키마 변경 대응에 사용한다.

Elasticsearch 인덱싱·검색·집계 구현 예시

# pip install elasticsearch==8.*
from elasticsearch import Elasticsearch
from datetime import datetime, timezone

es = Elasticsearch(
    "http://localhost:9200",
    basic_auth=("elastic", "changeme"),  # 보안 설정 환경에 맞게 변경
    request_timeout=2.0,
)

INDEX = "products-v1"

def ensure_index():
    if not es.indices.exists(index=INDEX):
        es.indices.create(
            index=INDEX,
            settings={
                "number_of_shards": 3,
                "number_of_replicas": 1,
                "refresh_interval": "1s"
            },
            mappings={
                "properties": {
                    "id": {"type": "keyword"},
                    "name": {"type": "text", "analyzer": "standard"},
                    "price": {"type": "integer"},
                    "stock": {"type": "integer"},
                    "category": {"type": "keyword"},
                    "ts": {"type": "date"}
                }
            }
        )

def index_doc(doc: dict):
    doc["@indexed_at"] = datetime.now(timezone.utc)
    es.index(index=INDEX, id=doc["id"], document=doc)

def search_by_keyword(q: str):
    body = {
        "query": {
            "multi_match": {
                "query": q,
                "fields": ["name^2", "category"]
            }
        },
        "aggs": {
            "by_category": {"terms": {"field": "category", "size": 10}}
        },
        "size": 10
    }
    return es.search(index=INDEX, body=body)

if __name__ == "__main__":
    ensure_index()
    index_doc({"id": "A100", "name": "Sample Phone", "price": 1000, "stock": 5, "category": "mobile", "ts": "2025-01-01T00:00:00Z"})
    resp = search_by_keyword("phone")
    print(resp["hits"]["hits"][0]["_source"])
    print(resp["aggregations"]["by_category"])

무중단 재인덱싱에는 인덱스 버전 별칭을 사용해 productsproducts-v1에 바인딩하는 방식을 권장한다. refresh_interval은 쓰기와 검색 성능의 균형을 맞추는 설정이다.

운영에서 확인할 Redis와 Elasticsearch의 차이

Redis에서는 maxmemory, eviction policy인 allkeys-lru·volatile-ttl 등을 설정하고 키 크기와 객체 직렬화를 최적화한다. orjson, snappy/zstd도 검토 대상이다. 대형 값은 sharding 또는 RedisJSON/Streams를 검토하고 메모리 파편화를 모니터링한다.

지속성과 가용성 측면에서 RDB 스냅샷은 읽기 성능에 유리하고, AOF는 내구성을 강화한다. AOF everysec와 파일 재작성(auto-rewrite) 설정을 권장한다. Sentinel은 자동 장애 조치에, Cluster는 수평 확장에 사용하며 크로스존 복제에는 네트워크 레이턴시를 고려한다. 분산 락에는 만료와 연장 로직이 필요하고, Redlock을 DB 트랜잭션 대체 목적으로 사용하는 것은 지양한다.

Elasticsearch는 필드 타입을 keyword·text·numeric·date로 정확하게 지정하고 불필요한 필드는 indexing: false로 둔다. 과도한 샤드 설정은 피하며, 초기 샤드는 데이터 크기와 성능 목표를 기준으로 산정한다.

쓰기 성능은 Bulk API의 5–15MB payload와 5–10 동시성부터 조정한다. 초기 적재 때 replica=0으로 두고 뒤에 replica를 복원하는 전략도 가능하다. 검색에서는 filter context로 캐시를 최적화하고 doc_values, 정렬 필드, prefilter shard 설정을 관리한다.

ILM으로 hot-warm-cold 계층을 구성하고 롤오버와 삭제를 관리한다. 스냅샷 저장소는 S3/GCS/NFS에 둘 수 있다. TLS/BasicAuth/RBAC을 활성화하고, 공개망에 노출하지 않으며 전용 VPC/SG를 사용한다. 버전 호환 플러그인 최신 정보도 확인해야 한다.

항목 Redis Elasticsearch
성능 서브밀리초 단건 읽기/쓰기, 싱글 스레드 이벤트 루프 밀리초 단위 검색/집계, 분산 병렬 실행
확장성 Cluster 해시 슬롯 기반 샤딩, 키 기반 수평 확장 샤드/레플리카, 데이터/ingest/master 롤 분리
일관성 단일 노드 강한 일관성, 클러스터 키 이동 시 주의 최종 일관성, refresh/replication 설정 영향
안정성 RDB/AOF 지속성, Sentinel/Cluster 장애 조치 Translog/replica ack, 스냅샷 복구
운영 편의 설정 간결, 메모리 튜닝 핵심 스키마/샤드/GC/ILM 등 운영 파라미터 다수

용량과 비용을 가늠하는 기준

캐시 메모리는 객체 크기 2.5KB(데이터 2KB + 오버헤드 0.5KB), 대상 키 1,200,000개라면 필요 메모리가 약 3.0GB다. 여유 30% 버퍼를 적용하면 3.9GB이며, maxmemory 4–6GB로 시작한 뒤 evict 지표를 보고 조정한다.

ES는 원시 데이터 500GB/일을 7일 보존하고 압축률을 40%로 가정할 때 저장 공간이 약 2.1TB다. 핫 티어 3노드, 노드당 64GB RAM과 2TB NVMe, 인덱스당 6 shards(노드당 2개)로 시작할 수 있다. 검색 부하가 늘면 replica를 추가한다.

Outbox와 CDC로 데이터 스큐를 줄인다

트랜잭션 아웃박스에서는 애플리케이션 DB 트랜잭션 안에 비즈니스 데이터와 아웃박스 레코드를 함께 기록한다. 이어서 Debezium/Connector → Kafka → Logstash/Filebeat → ES Bulk 경로로 변경 이벤트를 전달한다.

CDC 이벤트를 받으면 해당 키를 무효화하거나 키 버전을 증가시킨다. 이 흐름은 캐시, 검색 인덱스, DB 사이의 스큐를 최소화하는 데 사용한다.

Redis는 읽기 경로 최적화와 실시간 제어 흐름에 적합하고, Elasticsearch는 텍스트 검색과 집계 중심 분석에 맞는다. TTL·무효화·락, 인덱스·샤드 설계, Outbox·CDC·Bulk 파이프라인을 함께 정립한 뒤 점진적으로 도입한다. 환경별 벤치마크와 최신 버전 호환성 확인도 필요하다.

RedisElasticsearchNoSQL캐싱검색 분석