ElastiCache Redis 도입 시점: RDS 반복 읽기 쿼리 성능 개선 실전 가이드

RDS 인스턴스의 CPU와 커넥션 수가 치솟는데 슬로우 쿼리 로그를 열어보면 항상 같은 쿼리들이 반복해서 찍혀 있다. 상품 목록, 사용자 세션, 설정값처럼 데이터는 거의 바뀌지 않는데 매 요청마다 DB를 치고 있는 상황 — ElastiCache Redis를 캐시 레이어로 추가하는 것이 ElastiCache Redis 도입의 가장 전형적인 시나리오다.

TL;DR — ElastiCache Redis 도입 판단 기준

판단 항목Redis 도입 적합도입 불필요
동일 쿼리 반복 비율전체 읽기의 30% 이상이 동일 키대부분 유니크 쿼리
데이터 변경 주기수 분 ~ 수 시간 단위실시간 갱신 필수
RDS 병목 지점읽기 쿼리 과부하쓰기 병목, 스키마 문제
응답 지연 허용 범위수십 ms 이내 목표DB 직접 조회로 충분
캐시 일관성 요구Eventually Consistent 허용Strong Consistency 필수

ElastiCache Redis가 RDS 부하를 줄이는 원리

Redis는 인메모리 데이터 구조 저장소다. RDS가 디스크 I/O와 쿼리 파싱, 실행 계획 수립을 거쳐 결과를 반환하는 동안, Redis는 메모리에서 키-값을 직접 조회해 반환한다. 이 구조적 차이가 응답 속도 격차를 만든다.

캐시 히트 시 요청은 RDS에 도달하지 않는다. RDS 입장에서는 해당 쿼리가 존재하지 않는 것과 같다. 커넥션 풀 소모도 없고, 실행 계획 수립 비용도 없다. 트래픽이 몰리는 구간에서 RDS 커넥션 수가 안정적으로 유지되는 이유가 여기에 있다.

graph LR App["애플리케이션 서버"] Redis["ElastiCache Redis"] RDS["Amazon RDS"] App -->|"① GET cache_key"| Redis Redis -->|"② 캐시 히트: 즉시 반환"| App App -->|"④ 캐시 미스: SELECT 쿼리"| RDS RDS -->|"⑤ 쿼리 결과 반환"| App App -->|"⑥ SET cache_key TTL=300"| Redis App -->|"⑦ 데이터 변경 시 DEL cache_key"| Redis
  1. 캐시 히트 경로 (①②③): 애플리케이션이 Redis에 키를 조회하고 값이 존재하면 즉시 반환. RDS 요청 없음.
  2. 캐시 미스 경로 (①④⑤⑥): Redis에 키가 없으면 RDS를 조회하고, 결과를 Redis에 저장(SET with TTL)한 뒤 반환.
  3. 캐시 무효화 경로 (⑦): 데이터 변경 시 해당 키를 Redis에서 삭제하거나 갱신해 stale 데이터 제공을 방지.

ElastiCache Redis 클러스터 구성 — 핵심 파라미터

ElastiCache for Redis를 생성할 때 결정해야 할 핵심 항목은 클러스터 모드 활성화 여부, 노드 타입, 레플리카 수다. 클러스터 모드를 비활성화하면 단일 샤드에 최대 5개의 읽기 레플리카를 붙일 수 있다. 클러스터 모드를 활성화하면 데이터를 여러 샤드에 분산할 수 있지만, 애플리케이션 레벨에서 멀티키 연산 처리 방식을 고려해야 한다.

읽기 캐시 용도라면 클러스터 모드 비활성화 + 읽기 레플리카 구성이 운영 복잡도 대비 충분한 경우가 많다. 레플리카는 장애 조치(Failover) 대상이 되기도 하므로 프로덕션에서는 최소 1개 이상을 권장한다.

# ElastiCache Redis 복제 그룹 생성 (클러스터 모드 비활성화, 레플리카 1개)
aws elasticache create-replication-group \
  --replication-group-id my-redis-rg \
  --replication-group-description "RDS read cache layer" \
  --engine redis \
  --cache-node-type cache.r7g.large \
  --num-cache-clusters 2 \
  --automatic-failover-enabled \
  --at-rest-encryption-enabled \
  --transit-encryption-enabled \
  --cache-subnet-group-name my-cache-subnet-group \
  --security-group-ids sg-0123456789abcdef0 \
  --region us-east-1

위 명령은 프라이머리 1개 + 레플리카 1개로 구성된 복제 그룹을 생성한다. --num-cache-clusters는 전체 노드 수(프라이머리 포함)를 의미한다. 서브넷 그룹과 보안 그룹은 사전에 생성되어 있어야 한다.

# 생성된 복제 그룹의 엔드포인트 확인
aws elasticache describe-replication-groups \
  --replication-group-id my-redis-rg \
  --query 'ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint' \
  --region us-east-1

Cache-Aside 패턴 구현 — Python 예시

ElastiCache Redis 도입 시 가장 일반적으로 사용하는 패턴은 Cache-Aside(Lazy Loading)다. 애플리케이션이 캐시를 직접 관리하며, 캐시 미스 시 DB를 조회하고 결과를 캐시에 저장한다. 캐시 장애가 발생해도 DB로 폴백되므로 가용성 측면에서 안전하다.

🔽 Cache-Aside 패턴 전체 코드 보기
import redis
import json
import boto3

# ElastiCache 프라이머리 엔드포인트 연결
# transit-encryption-enabled 설정 시 ssl=True 필요
cache = redis.Redis(
    host='my-redis-rg.xxxxxx.ng.0001.use1.cache.amazonaws.com',
    port=6379,
    ssl=True,
    decode_responses=True
)

def get_product(product_id: str, db_conn):
    cache_key = f"product:{product_id}"
    
    # 1. 캐시 조회
    cached = cache.get(cache_key)
    if cached:
        return json.loads(cached)  # 캐시 히트
    
    # 2. 캐시 미스 — DB 조회
    row = db_conn.execute(
        "SELECT * FROM products WHERE id = %s",
        (product_id,)
    ).fetchone()
    
    if row is None:
        return None
    
    result = dict(row)
    
    # 3. 결과를 캐시에 저장 (TTL: 300초)
    cache.set(cache_key, json.dumps(result), ex=300)
    
    return result

def update_product(product_id: str, data: dict, db_conn):
    # DB 업데이트
    db_conn.execute(
        "UPDATE products SET name=%s, price=%s WHERE id=%s",
        (data['name'], data['price'], product_id)
    )
    db_conn.commit()
    
    # 캐시 무효화 — stale 데이터 방지
    cache_key = f"product:{product_id}"
    cache.delete(cache_key)

TTL 설정은 데이터 특성에 따라 다르게 가져가야 한다. 상품 정보처럼 변경이 드문 데이터는 수 분에서 수십 분, 실시간성이 중요한 재고 수량 같은 데이터는 수 초 단위로 짧게 설정하거나 캐싱 자체를 재고한다.

ElastiCache Redis 도입 전 확인해야 할 RDS 지표

Redis를 붙이기 전에 RDS에서 실제로 무슨 일이 일어나고 있는지 확인하지 않으면 엉뚱한 문제를 캐시로 해결하려다 실패한다. 슬로우 쿼리가 인덱스 누락 때문이라면 Redis가 아니라 인덱스가 답이다.

# RDS DatabaseConnections 지표 — 최근 1시간, 5분 단위
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name DatabaseConnections \
  --dimensions Name=DBInstanceIdentifier,Value=my-rds-instance \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 \
  --statistics Average \
  --region us-east-1
# RDS ReadIOPS 지표 확인
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name ReadIOPS \
  --dimensions Name=DBInstanceIdentifier,Value=my-rds-instance \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 \
  --statistics Average \
  --region us-east-1

DatabaseConnections가 지속적으로 높고, ReadIOPS가 쓰기 대비 압도적으로 높으며, Performance Insights에서 동일한 SELECT 쿼리가 반복 등장한다면 캐시 레이어 도입의 명확한 신호다.

캐시 히트율 모니터링 — ElastiCache 지표

Redis를 붙였다고 끝이 아니다. 캐시 히트율이 낮으면 RDS 부하는 거의 줄지 않는다. ElastiCache는 CacheHitsCacheMisses 지표를 CloudWatch로 제공한다.

# ElastiCache CacheHits 지표 확인
aws cloudwatch get-metric-statistics \
  --namespace AWS/ElastiCache \
  --metric-name CacheHits \
  --dimensions Name=ReplicationGroupId,Value=my-redis-rg \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 \
  --statistics Sum \
  --region us-east-1
# ElastiCache CacheMisses 지표 확인
aws cloudwatch get-metric-statistics \
  --namespace AWS/ElastiCache \
  --metric-name CacheMisses \
  --dimensions Name=ReplicationGroupId,Value=my-redis-rg \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 \
  --statistics Sum \
  --region us-east-1

히트율 = CacheHits / (CacheHits + CacheMisses). 이 값이 70% 미만이라면 캐시 키 설계나 TTL 전략을 재검토해야 한다. 키가 너무 세분화되어 있거나 TTL이 지나치게 짧은 경우가 대부분이다.

graph TD Start["ElastiCache Redis 운영 중"] HitCheck{"CacheHits 비율 확인"} HighHit["정상: RDS 부하 감소 확인"] LowHit["캐시 키 설계 / TTL 재검토"] EvictCheck{"Evictions 증가?"} EvictYes["노드 타입 업그레이드
또는 TTL 단축"] ConnCheck{"CurrConnections 급증?"} ConnYes["Redis 커넥션 풀 설정 확인"] Start --> HitCheck HitCheck -->|"70% 이상"| HighHit HitCheck -->|"70% 미만"| LowHit HighHit --> EvictCheck EvictCheck -->|"Yes"| EvictYes EvictCheck -->|"No"| ConnCheck ConnCheck -->|"Yes"| ConnYes ConnCheck -->|"No"| Start
  1. CacheHits 상승, DatabaseConnections 하락: 캐시가 정상적으로 RDS 부하를 흡수하고 있는 상태.
  2. Evictions 증가: 메모리 부족으로 캐시 데이터가 강제 삭제되고 있음. 노드 타입 업그레이드 또는 TTL 단축 검토.
  3. CurrConnections 급증: 애플리케이션에서 Redis 커넥션을 제대로 반환하지 않는 경우. 커넥션 풀 설정 확인.

실제 장애 사례 — 캐시가 붙었는데 RDS가 여전히 느린 이유

Redis를 붙이고 배포했는데 RDS CPU가 전혀 줄지 않았다. 처음엔 캐시 키 설계 문제라고 생각했다. CloudWatch에서 CacheHits를 확인하니 수치가 0에 가까웠다.

원인은 보안 그룹이었다. 애플리케이션 서버의 아웃바운드 규칙은 열려 있었지만, ElastiCache 보안 그룹의 인바운드 규칙에 애플리케이션 서버 보안 그룹이 소스로 등록되어 있지 않았다. Redis 연결 자체가 실패하고 있었고, 애플리케이션 코드의 예외 처리가 Redis 연결 실패 시 조용히 DB로 폴백하도록 작성되어 있었다.

연결 실패가 로그에 남지 않으면 캐시가 동작하지 않는다는 사실을 모른 채 며칠을 보낼 수 있다. Redis 클라이언트의 연결 오류 로깅은 반드시 명시적으로 활성화해야 한다.

# ElastiCache 보안 그룹 인바운드 규칙 확인
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0 \
  --query 'SecurityGroups[0].IpPermissions' \
  --region us-east-1
# 애플리케이션 서버 SG를 ElastiCache SG 인바운드에 추가 (포트 6379)
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 6379 \
  --source-group sg-0fedcba9876543210 \
  --region us-east-1

캐시 연결 실패는 조용히 폴백된다. 지표가 없으면 보이지 않는다.

IAM 및 보안 구성

ElastiCache for Redis는 Redis AUTH 또는 IAM 인증(RBAC)을 통해 접근을 제어할 수 있다. 전송 중 암호화(transit-encryption-enabled)와 저장 시 암호화(at-rest-encryption-enabled)는 클러스터 생성 시 활성화해야 하며, 생성 후 변경이 제한된다.

애플리케이션 서버가 ElastiCache에 접근하기 위해 별도의 IAM 정책이 필요한 것은 아니다. ElastiCache 접근은 네트워크 레벨(보안 그룹, VPC)과 Redis 인증(AUTH 토큰 또는 사용자)으로 제어된다. 다만 ElastiCache 리소스를 관리(생성, 수정, 삭제)하는 IAM 역할에는 최소 권한 원칙을 적용해야 한다.

🔽 ElastiCache 관리용 최소 IAM 정책 예시 보기
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ElastiCacheReadOnly",
      "Effect": "Allow",
      "Action": [
        "elasticache:Describe*",
        "elasticache:List*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ElastiCacheModify",
      "Effect": "Allow",
      "Action": [
        "elasticache:ModifyReplicationGroup",
        "elasticache:AddTagsToResource"
      ],
      "Resource": "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-redis-rg"
    }
  ]
}

ElastiCache Redis 도입 마무리 및 다음 단계

ElastiCache Redis 도입은 RDS 반복 읽기 병목을 해소하는 검증된 방법이지만, 캐시 키 설계, TTL 전략, 무효화 로직이 함께 갖춰져야 실질적인 효과가 난다. Redis를 붙이는 것보다 '무엇을 캐시하고 언제 무효화할지'를 결정하는 것이 더 어렵다.

다음 단계로 고려할 수 있는 항목:

  • ElastiCache Serverless 옵션 검토 — 트래픽 패턴이 불규칙한 경우 용량 관리 부담 감소
  • CloudWatch 알람 설정 — Evictions, CurrConnections, EngineCPUUtilization 임계값 기반
  • RDS Read Replica 병행 검토 — 캐시로 해결되지 않는 복잡한 읽기 쿼리 분산
  • 공식 문서: Amazon ElastiCache for Redis 공식 문서

핵심 용어 정리

용어설명
Cache-Aside (Lazy Loading)애플리케이션이 캐시 미스 시 직접 DB를 조회하고 결과를 캐시에 저장하는 패턴
TTL (Time To Live)캐시 항목의 유효 시간. 만료 후 자동 삭제됨
캐시 히트율전체 캐시 요청 중 캐시에서 데이터를 찾은 비율. 높을수록 DB 부하 감소
EvictionRedis 메모리 한도 초과 시 정책에 따라 기존 키를 삭제하는 동작
복제 그룹 (Replication Group)ElastiCache에서 프라이머리와 레플리카 노드를 묶는 논리적 단위

Related Posts

댓글

이 블로그의 인기 게시물

EC2 SSH 연결 타임아웃 완전 해결 가이드: Security Group 인바운드 규칙부터 라우팅까지

EC2 SSH 연결 시간 초과: 확인해야 할 보안 그룹(Security Group) 규칙

IAM User vs IAM Role 차이점 완전 정리 — EC2에서 S3 접근 시 무엇을 써야 하는가