개요
이 문서는 Redis를 사용한 데이터베이스 캐싱 전략에 대한 AWS 백서를 정리한 내용입니다. 인메모리 데이터 캐싱은 애플리케이션 성능을 향상시키고 데이터베이스 비용을 절감하는 가장 효과적인 전략 중 하나입니다.

1. 데이터베이스의 주요 과제
분산 애플리케이션을 구축할 때 디스크 기반 데이터베이스는 여러 문제점을 가지고 있습니다.
1.1 느린 쿼리 처리
- 디스크에서 데이터를 검색하는 물리적 시간과 쿼리 처리 시간으로 인해 응답 시간이 밀리초 단위로 증가
- 최적화된 상태에서도 최소 수십 밀리초의 응답 시간 소요
1.2 확장 비용
- NoSQL이든 관계형 데이터베이스든 높은 읽기 성능을 위한 확장 비용이 높음
- 단일 인메모리 캐시 노드가 제공하는 초당 요청 수를 맞추려면 여러 개의 데이터베이스 읽기 복제본이 필요
1.3 데이터 접근의 복잡성
- 관계형 데이터베이스는 데이터 모델링에는 우수하지만 데이터 접근에는 최적화되어 있지 않음
- 특정 구조나 뷰로 데이터에 접근하고자 할 때 성능 저하 발생
2. 데이터베이스 캐싱의 유형
데이터베이스 캐시는 데이터베이스, 애플리케이션 내부, 또는 독립적인 계층 등 여러 위치에 존재할 수 있습니다.
2.1 데이터베이스 통합 캐시 (Database-integrated caches)
- Amazon Aurora와 같은 일부 데이터베이스는 내장 캐시 제공
- 데이터베이스 엔진 내에서 관리되며 자동으로 write-through 기능 제공
- 기본 데이터가 변경되면 자동으로 캐시 업데이트
- 단점: 크기와 기능이 데이터베이스 인스턴스에 할당된 메모리로 제한됨
2.2 로컬 캐시 (Local caches)
- 애플리케이션 내에서 자주 사용되는 데이터를 저장
- 네트워크 트래픽을 제거하여 빠른 데이터 검색 가능
- 단점:
- 각 노드가 독립적으로 작동하여 데이터 공유 불가
- 여러 애플리케이션 서버 간 데이터 동기화가 어려움
- 장애 발생 시 캐시 데이터 손실
2.3 원격 캐시 (Remote caches)
- Redis나 Memcached와 같은 전용 서버에 캐시 데이터 저장
- 초당 수십만에서 백만 건의 요청 처리 가능
- 평균 응답 시간이 밀리초 이하
- Amazon ElastiCache와 같은 서비스는 고가용성 제공
- 장점:
- 분산 환경에 이상적
- 모든 시스템이 활용할 수 있는 연결된 클러스터로 작동
- 네트워크 지연이 문제가 될 때 로컬 캐시와 함께 사용 가능
3. 캐싱 패턴
3.1 Cache-Aside (Lazy Loading)
가장 일반적인 캐싱 전략으로, 데이터가 요청된 후에 캐시를 업데이트하는 반응적 접근 방식입니다.
동작 방식:
- 애플리케이션이 데이터베이스에서 데이터를 읽어야 할 때 먼저 캐시 확인
- 데이터가 있으면(캐시 히트) 캐시된 데이터 반환
- 데이터가 없으면(캐시 미스) 데이터베이스 쿼리 후 캐시에 저장하고 반환
장점:
- 캐시에 실제로 요청된 데이터만 저장되어 비용 효율적
- 구현이 간단하고 즉각적인 성능 향상 제공
단점:
- 캐시 미스 시 추가 왕복으로 인한 초기 응답 시간 오버헤드 발생
3.2 Write-Through
데이터베이스 업데이트 후 즉시 캐시를 업데이트하는 선제적 접근 방식입니다.
동작 방식:
- 애플리케이션, 배치 또는 백엔드 프로세스가 주 데이터베이스 업데이트
- 즉시 캐시의 데이터도 업데이트
장점:
- 캐시가 항상 최신 상태로 유지되어 캐시 히트율 향상
- 전체 애플리케이션 성능과 사용자 경험 개선
- 데이터베이스 읽기 부하 감소
단점:
- 자주 요청되지 않는 데이터도 캐시에 저장되어 더 크고 비용이 높은 캐시 필요
참고: Write-through 패턴은 거의 항상 lazy loading과 함께 구현됩니다. 캐시 미스가 발생하면 lazy loading 패턴으로 캐시를 업데이트합니다.
4. 캐시 유효성 관리
4.1 TTL(Time To Live) 설정
캐시 키에 만료 시간을 적용하여 데이터의 최신성을 제어할 수 있습니다.
고려사항:
- 데이터 변경 빈도 이해: 기본 데이터의 변경 속도를 파악
- 오래된 데이터 위험 평가: 최신 데이터 대신 오래된 데이터가 반환될 위험 평가
적용 예시:
- 정적/참조 데이터:
- 자주 업데이트되지 않는 데이터
- 더 긴 TTL 적용
- Write-through 방식으로 업데이트
- 동적 데이터:
- 자주 변경되는 데이터
- 짧은 TTL 적용
- 주 데이터베이스의 변경 속도에 맞춤
4.2 TTL Jitter 추가
문제점: 모든 캐시 데이터가 동시에 만료되면 데이터베이스에 과도한 부하 발생
해결책: TTL에 무작위 시간 값(jitter) 추가
TTL = 기본 TTL 값(초) + 무작위 jitter 값
효과:
- 백엔드 데이터베이스의 부하 분산
- 만료된 키 삭제로 인한 캐시 엔진의 CPU 사용량 감소
5. 캐시 제거 정책 (Evictions)
캐시 메모리가 가득 차거나 maxmemory 설정을 초과하면 제거 정책에 따라 키가 제거됩니다.
5.1 제거 정책 유형
Amazon ElastiCache(Redis OSS)의 기본 정책은 volatile-lru입니다.
| 정책 | 설명 |
| allkeys-lru | TTL 설정과 관계없이 가장 최근에 사용되지 않은(LRU) 키 제거 |
| allkeys-lfu | TTL 설정과 관계없이 가장 사용 빈도가 낮은(LFU) 키 제거 |
| volatile-lru | TTL이 설정된 키 중 가장 최근에 사용되지 않은 키 제거 |
| volatile-lfu | TTL이 설정된 키 중 가장 사용 빈도가 낮은 키 제거 |
| volatile-ttl | TTL이 가장 짧은 키 제거 |
| volatile-random | TTL이 설정된 키 중 무작위로 제거 |
| allkeys-random | TTL 설정과 관계없이 무작위로 키 제거 |
| noeviction | 키를 제거하지 않음. 메모리가 부족하면 쓰기 차단 |
5.2 제거 정책 선택 전략
- 기본 캐싱: LRU 기반 정책이 일반적
- 특정 요구사항: TTL 또는 무작위 기반 정책 고려
- 제거 발생 시:
- 노드를 더 큰 메모리로 확장(scale up)
- 클러스터에 노드 추가(scale out)
- 예외: LRU 캐시로 의도적으로 제거를 활용하는 경우
5.3 LFU(Least Frequently Used) 정책
Redis는 시간 기반 LRU 외에도 LFU 제거 정책을 지원합니다.
특징:
- 접근 빈도를 기반으로 키 제거
- 각 객체의 접근 카운터 추적
- 일정 기간(decay period) 후 카운터 감소
- 자주 사용되는 데이터를 메모리에 유지하여 더 높은 캐시 히트율 제공
6. 관계형 데이터베이스 캐싱 기법
관계형 데이터베이스에서 데이터를 캐싱하는 다양한 방법을 살펴봅니다.
예시 시나리오
다음 SQL 쿼리를 가정합니다:
SELECT FIRST_NAME, LAST_NAME, EMAIL, CITY, STATE, ADDRESS, COUNTRY
FROM CUSTOMERS
WHERE CUSTOMER_ID = "1001";
6.1 데이터베이스 SQL ResultSet 캐싱
방법: 직렬화된 ResultSet 객체를 캐시에 저장
장점:
- DAO(Data Access Object) 계층과 같이 데이터 검색 로직이 추상화된 경우 유용
- ResultSet 객체를 반복 처리할 수 있어 통합 로직 최소화
- 모든 관계형 데이터베이스에 적용 가능
단점:
- ResultSet에서 값을 추출해야 하므로 데이터 접근이 단순화되지 않음
- 지연 시간 감소에만 기여
구현 예시 (Python):
if not r.exists(pickle.dumps(key)):
try:
cursor.execute(key)
results = cursor.fetchall()
r.set(pickle.dumps(key), pickle.dumps(results))
r.expire(pickle.dumps(key), ttl)
data = results
except:
print("Error: unable to fetch data.")
else:
data = pickle.loads(r.get(pickle.dumps(key)))
구현 예시 (Java):
if (rs != null) {
CachedRowSet cachedRowSet = new CachedRowSetImpl();
cachedRowSet.populate(rs, 1);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutput out = new ObjectOutputStream(bos);
out.writeObject(cachedRowSet);
byte[] redisRSValue = bos.toByteArray();
jedis.set(key.getBytes(), redisRSValue);
jedis.expire(key.getBytes(), ttl);
}
6.2 특정 필드와 값을 사용자 정의 형식으로 캐싱
방법: 데이터베이스 행의 일부를 JSON이나 XML 같은 구조로 캐시
장점:
- 구현이 간단
- 애플리케이션의 데이터 접근 패턴에 맞는 형식 선택 가능
단점:
- 데이터 쿼리 시 서로 다른 유형의 객체 사용 (Redis 문자열과 데이터베이스 결과)
- 개별 속성을 검색하려면 전체 구조를 파싱해야 함
구현 예시 (Python):
try:
cursor.execute(query)
results = cursor.fetchall()
for row in results:
customer = {
"FirstName": row["FirstName"],
"LastName": row["LastName"]
}
r.set("customer:id:" + str(row["id"]), json.dumps(customer))
except:
print("Error: Unable to fetch data.")
6.3 Redis 집계 데이터 구조로 캐싱
방법: 데이터베이스 행을 Redis Hash와 같은 데이터 구조로 변환
장점:
- ResultSet 반복이나 JSON 파싱 오버헤드 제거
- Redis Hash의 경우 개별 속성 쿼리 가능
- 데이터 접근 패턴 단순화
- 속성 추가/삭제 등 다양한 기능 제공
단점:
- 데이터 쿼리 시 서로 다른 유형의 객체 사용
구현 예시 (Python):
try:
cursor.execute(query)
customer = cursor.fetchall()
r.hset("customer:id:" + str(customer["id"]), "FirstName", customer[0]["FirstName"])
r.hset("customer:id:" + str(customer["id"]), "LastName", customer[0]["LastName"])
except:
print("Error: Unable to fetch data.")
구현 예시 (Java):
while (rs.next()) {
Customer customer = new Customer();
Map<String, String> map = new HashMap<String, String>();
customer.setFirstName(rs.getString("FIRST_NAME"));
map.put("firstName", customer.getFirstName());
customer.setLastName(rs.getString("LAST_NAME"));
map.put("lastName", customer.getLastName());
jedis.hmset("customer:id:" + customer.getCustomerID(), map);
}
6.4 직렬화된 애플리케이션 객체 엔티티 캐싱
방법: 애플리케이션 객체를 직렬화하여 캐시에 저장
장점:
- 간단한 직렬화/역직렬화 기술로 네이티브 애플리케이션 상태의 객체 사용
- 데이터 변환 로직 최소화로 애플리케이션 성능 향상
단점:
- 고급 애플리케이션 개발 사용 사례
구현 예시 (Python):
try:
cursor.execute(query)
results = cursor.fetchall()
r.set(pickle.dumps(key), pickle.dumps(results))
r.expire(pickle.dumps(key), ttl)
except:
print("Error: Unable to fetch data.")
구현 예시 (Java):
Customer customer = (Customer) object;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutput out = null;
try {
out = new ObjectOutputStream(bos);
out.writeObject(customer);
out.flush();
byte[] objectValue = bos.toByteArray();
jedis.set(key.getBytes(), objectValue);
jedis.expire(key.getBytes(), ttl);
}
7. Redis를 활용한 추가 캐싱
7.1 Amazon S3 객체 캐싱
Redis는 전통적으로 데이터베이스 캐시 용도로 사용되지만, Amazon S3와 같은 스토리지 서비스의 객체도 캐싱할 수 있습니다.
사용 사례:
- 데이터 레이크, 미디어 카탈로그, 웹사이트 관련 콘텐츠
- 10ms 이하의 지연 시간 요구사항
- 전체 저장 데이터의 1-10%에 대한 빈번한 요청
이점:
- 5ms 미만의 일관되고 낮은 지연 시간 유지
- S3 검색 비용 절감
- 성능 목표 달성
참고 자료: Turbocharge Amazon S3 with Amazon ElastiCache
8. Amazon ElastiCache와 자체 관리 Redis
8.1 ElastiCache의 장점
Redis는 가장 인기 있는 오픈소스 인메모리 데이터 스토어입니다. Amazon ElastiCache는 완전 관리형 Redis 서비스를 제공합니다.
주요 이점:
- 클러스터 관리의 모든 관리 작업(모니터링, 패치, 백업, 자동 장애 조치) 자동화
- 비즈니스와 데이터에 집중 가능
- 오픈소스 버전과 완전 호환되면서 추가 안정성과 견고성 제공
- 제거 정책, 버퍼 제한 등 파라미터 수정 용이
- 테라바이트 규모로 클러스터 확장 및 크기 조정
- Amazon VPC 내에서 클러스터 격리 가능한 강화된 보안
8.2 Redis 엔진 지원
HIPAA 규정 준수 애플리케이션 구축 가능:
- Redis 3.2.6 이상 버전 사용 시
- 저장 데이터 암호화 지원
- 전송 중 암호화 지원
- Redis AUTH 지원
RBAC(역할 기반 액세스 제어):
- Redis 6.x 버전에서 지원
- Redis AUTH 대신 사용 가능
- 액세스 문자열로 특정 권한을 가진 사용자 생성
- 사용 사례별 역할에 맞춰 사용자 그룹 할당
동적 네트워크 처리 (버전 5.0.3 이상):
- 4개 이상의 vCPU를 가진 노드에서 추가 CPU 활용
- 노드당 처리량 최대 83% 증가
- 노드당 지연 시간 최대 47% 감소
지원 버전: Redis 6.0.5 이하 (발행 시점 기준)
8.3 사용 가능한 인스턴스 유형
현재 세대:
- 이전 세대 대비 더 많은 메모리와 연산 능력, 저렴한 비용
Graviton2 기반 인스턴스:
- M6g 및 R6g 인스턴스 패밀리
- 초저지연 및 높은 처리량 제공
- 이전 세대 대비 최대 45% 가격 대비 성능 개선
- ElastiCache 고객의 기본 선택
버스터블 T 표준 노드:
- 기본 수준의 CPU 성능 제공
- CPU 크레딧이 소진될 때까지 언제든지 CPU 사용량 버스트 가능
- CPU 크레딧은 전체 CPU 코어의 1분간 성능 제공
8.4 AWS Nitro System
개요:
- 차세대 EC2 인스턴스의 기반 플랫폼
- 더 빠른 혁신, 고객 비용 절감, 보안 강화 및 새로운 인스턴스 유형 제공
특징:
- 가상화 인프라의 완전한 재설계
- 하이퍼바이저 기능을 전용 하드웨어 및 소프트웨어로 분리
- 호스트 하드웨어의 거의 모든 연산 및 메모리 리소스를 인스턴스에 제공
- 전반적인 성능 향상
- 고속 네트워킹, 고속 EBS, I/O 가속을 위한 전용 Nitro 카드
ElastiCache에서 활용:
- M5 (cache.m5.xlarge 이상) 또는 R5 (cache.r5.xlarge 이상) 인스턴스 선택
9. Redis 클러스터 모드
9.1 클러스터 모드 개요
클러스터 모드 활성화 시:
- 클러스터 성능에 영향 없이 확장 가능
- 수평 확장(scale-out): 샤드 추가
- 수직 확장(scale-up/down): 노드 유형 변경
- 새 노드와 기존 노드 자동 동기화
클러스터 구성:
- Redis(단일 노드)
- Redis(클러스터 모드 비활성화)
- Redis(클러스터 모드 활성화)
9.2 확장성
클러스터 모드 비활성화:
- 복제 그룹에 최대 5개의 읽기 복제본
- 복제본 추가/제거 시 다운타임 없음
클러스터 모드 활성화:
- 기본적으로 최대 90개의 샤드 (요청 시 증가 가능)
- 각 노드 그룹에 최대 5개의 읽기 복제본
- 수평 확장으로 쓰기 용량 증가
- 예측 불가능한 네트워크 및 스토리지 요구사항이나 쓰기 집약적 워크로드에 적합
데이터 분산:
- 키 공간의 균등 분산
- 해시 슬롯을 클러스터 내 샤드에 분배
- 더 많은 노드에 워크로드 분산
- 기본적으로 해시 슬롯은 샤드 간 균등 분배
- 사용자 정의 해시 슬롯 구성 가능
- 피크 시간이 아닐 때 클러스터 크기 조정 권장
9.3 Reader Endpoint
기능:
- 클러스터 모드 비활성화 시 사용 가능
- 모든 읽기 트래픽을 단일 클러스터 수준 엔드포인트로 전달
이점:
- 들어오는 읽기 연결 요청을 모든 읽기 복제본에 균등 분배
- 개별 복제본에 트래픽을 직접 보낼 필요 제거
- 캐시된 데이터 조회 및 접근 구성 단순화
- 모든 읽기 트래픽 로드 밸런싱
- 서로 다른 AWS 가용 영역(AZ)에 읽기 복제본 배치로 고가용성 달성
10. Global Datastore
10.1 주요 사용 사례
목적:
- 글로벌 저지연 읽기
- 재해 복구 시나리오 지원
기능:
- 매우 낮은 지연 시간의 로컬 읽기 제공
- 동시에 클러스터 복원력 유지
- 주 리전 성능 저하 시 보조 리전으로 장애 조치
10.2 구조
Global Datastore 구성:
- 주(Primary) 클러스터: 쓰기 수락 및 모든 클러스터에 복제
- 보조(Secondary) 클러스터: 읽기 요청만 수락하고 주 클러스터의 데이터 업데이트 복제
사용 예시:
- 미디어 콘텐츠 애플리케이션
- 활성 클러스터에 쓰기
- 로컬 리전에서 동일한 콘텐츠 읽기
10.3 특징
안정성 및 보안:
- 안정적이고 안전한 완전 관리형 리전 간 복제
- 보조 클러스터를 주 클러스터로 쉽게 승격
확장성:
- 새 클러스터 또는 기존 클러스터에서 설정 가능
- 최신 Redis 엔진 버전(5.0.6 이상) 실행 필요
- 리전 클러스터를 수직 및 수평으로 확장 가능
- Global Datastore 수정 시 중단 없음
보안:
- 리전 간 통신을 위한 전송 중 암호화
- 저장 데이터 암호화 추가 지원
11. 워크로드 관련 크기 조정 모범 사례
11.1 온라인 확장
Redis 버전 5.0.5 이후:
- 다운타임 없이 온라인으로 확장/축소 가능
- 확장 중에도 클러스터가 온라인 상태 유지
- 들어오는 요청에 계속 응답
확장 방법:
- 수직 확장(Scale-up): 더 큰 노드 유형 선택으로 읽기/쓰기 용량 증가
- 수직 축소(Scale-down): 더 작은 노드 선택으로 용량 감소
- 수평 확장(Scale-out): 읽기 복제본 추가로 읽기 용량 증가
- 샤드 추가: 클러스터에 새 샤드 추가로 쓰기 용량 증가
11.2 중단 없는 확장을 위한 모범 사례
- ENI(Elastic Network Interface) 가용성 확인 (확장 시)
- 충분한 수의 네트워크 인터페이스 필요
- 충분한 메모리 확인 (축소 시)
- 더 작은 노드가 들어오는 트래픽을 수용할 수 있는 충분한 메모리 필요
- 트래픽이 최소인 시간에 확장 수행
- 확장 프로세스가 온라인으로 유지되도록 설계됨
- 새 노드로 데이터를 동기화하는 데 도움
- 개발 환경에서 테스트
- 가능한 경우 항상 애플리케이션을 개발 환경에서 먼저 테스트
12. 결론
현대 애플리케이션은 낮은 성능을 감수할 수 없습니다. 오늘날의 사용자는 느리게 실행되는 애플리케이션과 나쁜 사용자 경험에 대한 인내심이 낮습니다.
핵심 포인트:
- 낮은 지연 시간과 데이터베이스 확장이 애플리케이션 성공에 중요
- 데이터베이스 캐싱 사용이 필수적
Amazon ElastiCache (Redis OSS)의 장점:
- 다운타임 없는 확장성
- 리전별 저지연 엔드포인트를 위한 Global Datastore
- 클러스터형 분산 캐시 실행의 단순화된 접근
- 관련 관리 작업 최소화
- 엔지니어링 팀과 고객의 주요 선택
참고 자료
'Spring > 이론' 카테고리의 다른 글
| DDD에서 AbstractAggregateRoot 이해하기 (0) | 2025.10.30 |
|---|---|
| Spring Boot 환경에서의 직렬화(Serialization) 이해하기 (3) | 2025.10.22 |
| Entity 식별자 전략: UUID vs Auto Increment vs Snowflake (0) | 2025.10.01 |
| Spring Boot 검증 처리 아키텍처 전략: 컨트롤러에서 BindingResult를 직접 처리하지 말아야 하는 이유 (0) | 2025.09.16 |
| 데이터베이스 인덱스의 구조와 특징 (0) | 2025.06.18 |