05 Redis 캐싱 / 영속성
05 Redis 캐싱 / 영속성
1. Note
- 정답은 없다.
- 모든 상황에 대해서 케바케에 따라서 전략을 달리해야함.
- 하지만 어디부터 어디까지 케바케로 봐야할지 고민이 필요할듯.
- 뭐 그냥 상황마다 다르니까 그냥 여기는 그러겟지? 라고간다면
- 굉장히 대안없이 수용하는 모습이 될듯.
2. Reids 캐싱
1
2
3
4
5
6
redisTemplate.opsForValue().set(
CACHE_KEY_CATEGORY, // key: Redis에 저장될 키 (데이터 식별자)
cacheCategories, // value: 저장할 실제 데이터 (객체/List 등, 직렬화됨)
1, // timeout: 만료 시간 값
TimeUnit.HOURS // unit: 시간 단위 (여기서는 1시간 뒤 만료)
);
3. Redis - SpringBoot - 패턴 구현
1. Cache-aside (캐시-어사이드) 패턴 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Transactional(readOnly = true)
public List<CategoryResponse> findAllForCacheAside() {
// 캐시에서 카테고리 구조 데이터 조회 시도
String cachedCategories = redisTemplate.opsForValue().get(CACHE_KEY_CATEGORY);
// 캐시 히트
if (!ObjectUtils.isEmpty(cachedCategories)) {
log.info("Cache Hit: categoryStruct for key = {}", CACHE_KEY_CATEGORY);
return JsonUtil.fromJsonList(cachedCategories, CategoryResponse.class);
}
// 캐시 미스, 데이터베이스에서 조회 (findAll() 호출)
log.info("Cache Miss: categoryStruct for key = {}", CACHE_KEY_CATEGORY);
List<CategoryResponse> categories = findAll();
// 데이터베이스에서 조회한 데이터를 캐시에 저장
if (!categories.isEmpty()) {
String cacheCategories = JsonUtil.toJson(categories);
redisTemplate.opsForValue().set(CACHE_KEY_CATEGORY, cacheCategories,1,TimeUnit.HOURS);
}
return categories;
}
2. Write-through (라이트-스루) 전략
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Transactional
public void saveWriteThrough(CategoryRequest request) {
try {
create(request); // 데이터 베이스 신규 카테고리 저장
// 캐시 업데이트
try {
// 데이터를 한번 전체 로딩해버리면 필요한것만 분류 작업하거나 그런게 필요없음
// 그래서 그냥 전체 한번 로드해서 영속성 엔티티에 띄우고 캐싱함.
// findAll은 카테고리 전체 만드는 로직임. (지금 내부로직 중요 X)
List<CategoryResponse> categories = findAll();
if (!categories.isEmpty()) {
String cacheCategories = JsonUtil.toJson(categories);
redisTemplate.opsForValue().set(CACHE_KEY_CATEGORY, cacheCategories);
}
} catch (Exception e) {
log.error("Error updating cache key {}: {}", CACHE_KEY_CATEGORY, e.getMessage());
}
} catch (Exception e) {
log.error("Failed to save category with Write-through: {}", e.getMessage(), e);
}
}
3. Write-back (라이트-백) 전략
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Transactional
public void saveWriteBack(CategoryRequest request) {
try {
// 캐시에서 카테고리 구조 데이터 조회 시도
String cachedCategories = redisTemplate.opsForValue().get(CACHE_KEY_CATEGORY);
List<CategoryResponse> categories = new ArrayList<>();
// 캐싱 데이터가 있다면 캐싱 데이터 불러오기
if (!ObjectUtils.isEmpty(cachedCategories)) {
categories = JsonUtil.fromJsonList(cachedCategories, CategoryResponse.class);
}
// 캐시에 새로운 카테고리 데이터 추가
CategoryResponse newCacheCategory = CategoryResponse.builder()
.name(request.getName())
.childCategories(new ArrayList<>())
.build();
// ID가 auto-increment면 ID를 넣을 방법이 없음
// 그래서 back-pattern은 UUID로 처리하거나, 다른 전략을 사용해야함.
categories.add(newCacheCategory);
// Redis에 우선 저장
String cacheCategories = JsonUtil.toJson(categories);
redisTemplate.opsForValue().set(CACHE_KEY_CATEGORY, cacheCategories);
// 데이터베이스 저장 작업은 비동기로 처리
saveToDatabaseAsync(request);
} catch (Exception e) {
log.error("Write-back 패턴 저장 실패: {}", e.getMessage());
}
}
@Async // 비동기 방식
@Transactional(propagation = Propagation.REQUIRES_NEW) // 트랜잭션 분리
public void saveToDatabaseAsync(CategoryRequest request) {
try {
create(request);
} catch (Exception e) {
log.error("비동기 DB 저장 실패: {}", e.getMessage(), e);
}
}
4. Redis 최적화 - TTL
1. 만료 시간 설정 (TTL: Time To Live)
- TTL(Time To Live)
- Redis에 저장된 데이터가 얼마 동안 유지될지를 설정하는 시간
- 설정된 시간이 지나면 해당 데이터는 자동으로 삭제
2. 필요성
- 메모리 효율성
- 불필요하거나 더 이상 사용되지 않는 데이터를 자동으로 제거함으로써 Redis의 메모리 사용량을 안정적으로 유지
- Redis는 메모리 기반 저장소이기 때문에, 이러한 자동 정리 메커니즘은 자원 관리 측면에서 매우 중요
- 데이터 일관성 보조
- 캐시 데이터는 원본 데이터와 시점 차이가 발생할 수밖에 없음
- TTL을 설정하면 일정 시간이 지난 후 캐시가 제거되고, 이후 요청 시 최신 데이터를 다시 조회
- 이를 통해 데이터 불일치 문제를 완화
- 휘발성 데이터 관리
- 세션 정보, 인증 토큰, 일시적인 알림과 같이 유효 기간이 명확한 데이터는 TTL을 통해 자연스럽게 관리
- 별도의 삭제 로직 없이도 자동으로 만료되기 때문에 구현이 단순해지고 실수 가능성도 줄어듬
3. 명령어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# mykey를 "hello"로 설정하고 5분(300초) 후 만료
# 동일 명령어
SET mykey "hello" EX 300
SETEX mykey 300 "hello"
# session:user:123 키에 1시간(3600초)의 만료 시간 설정
EXPIRE session:user:123 3600
# temp_data 키를 5초(5000밀리초) 후 만료
PEXPIRE temp_data 5000
# mykey의 남은 만료 시간 확인
TTL mykey
# temp_data 키의 만료 시간을 제거
PERSIST temp_data
5. LRU & LFU
1. LRU
1. LRU (Least Recently Used)
- 가장 최근에 사용되지 않은 데이터를 먼저 삭제하는 방식
- 동작 방식
- 각 데이터는 “마지막으로 접근된 시간”을 기준으로 관리
- 오랫동안 조회되지 않은 데이터일수록 우선적으로 제거
- 특징
- 최근에 사용된 데이터는 다시 사용될 가능성이 높다는 가정 기반
- 구현이 단순하고 직관적
- 일반적인 캐시에서 가장 널리 사용됨
2. 장단점
- 장점
- 최근 사용된 데이터를 기준으로 하기 때문에 최신 트렌드 반영이 빠름
- 구현이 단순하고 대부분의 캐시 상황에서 안정적으로 동작
- 사용자 행동 변화(패턴 변화)에 빠르게 적응
- 단점
- 자주 사용되던 데이터라도 한동안 안 쓰이면 바로 삭제될 수 있음
- 단 한 번 사용된 데이터라도 최근이면 불필요하게 살아남을 수 있음
- “사용 빈도”를 고려하지 않음
2. LFU
1. LFU (Least Frequently Used)
- 가장 적게 사용된 데이터를 먼저 삭제하는 방식
- 동작 방식
- 각 데이터마다 “사용 횟수”를 카운트
- 사용 횟수가 적은 데이터부터 제거
- 특징
- “자주 사용되는 데이터는 중요하다”는 가정 기반
- 장기적인 사용 패턴을 반영
2. 장단점
- 장점
- 자주 사용되는 데이터를 유지하기 때문에 핵심 데이터 보존에 유리
- 반복 조회가 많은 서비스에서 캐시 효율이 높음
- 인기 데이터 중심 구조(랭킹, 조회수 등)에 적합
- 단점
- 과거에 많이 사용된 데이터가 계속 남아 최신 트렌드 반영이 느림
- 사용 패턴이 바뀌어도 적응 속도가 느림
- 구현 및 관리가 LRU보다 상대적으로 복잡
3. redis.conf(설정파일)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 스프링부트에서는 그냥 사용만하고,
# 레디스의 redis.conf파일에서 LRU / LFU 설정
# 최대 메모리 512MB 설정
maxmemory 512mb
# 모든 키에 대해 LRU 정책 사용 (가장 보편적)
maxmemory-policy allkeys-lru
# 또는 모든 키에 대해 LFU 정책 사용 (사용 빈도 기반)
# maxmemory-policy allkeys-lfu
# 또는 만료 시간이 설정된 키에 대해서만 LRU 사용
# maxmemory-policy volatile-lru
# 또는 메모리 가득 차면 쓰기 거부 (데이터 손실 없음)
# maxmemory-policy noeviction
6. Redis의 영속성
1. Redis의 영속성
- 데이터를 디스크에 저장하는 다양한 영속성(Persistence) 옵션을 제공
2. RDB 방식
1. RDB (Snapshot) 방식
- 특정 시점의 메모리 데이터를 통째로 복사해서 디스크에 파일(.rdb)로 저장하는 방식
- 카메라로 스냅샷을 찍듯이 현재 상태를 저장
2. 장단점
- 장점
- 파일 크기가 작고, 복구 속도가 매우 빠릅니다. 특히 대량의 데이터를 복구할 때 효율적
- 스냅샷 생성 과정이 백그라운드에서 수행되므로, Redis 서버의 성능 저하가 비교적 적음
- 단점
- 스냅샷 주기 사이의 변경 데이터는 유실될 수 있음(스냅샷과 스냅샷 사이에 롤백되는 경우)
- fork()로 인해 대용량 환경에서 CPU와 메모리 오버헤드가 발생할 수 있음
3. redis.conf
1
2
3
4
5
# 60초마다 최소 1000개의 키가 변경되었을 경우 스냅샷 저장
save 60 1000
# 300초(5분)마다 최소 10개의 키가 변경되었을 경우 스냅샷 저장
save 300 10
3. AOF 방식
1. AOF (Append-Only File) 방식
- 서버에서 발생하는 모든 쓰기(Write) 연산을 명령어 형태로 로그 파일(.aof)에 순차적으로 기록하는 방식
- 데이터베이스의 트랜잭션 로그와 유사하다고 볼 수 있음
2. 장단점
- 장점
- RDB보다 데이터 유실 가능성이 낮으며, 설정에 따라 유실 범위를 최소화할 수 있음
- 명령어 로그 기반으로 저장되어, 장애 발생 시 재실행을 통해 데이터 복구가 가능
- 단점
- 모든 쓰기 연산을 기록하므로 RDB보다 파일 크기가 커질 수 있음
- 쓰기 작업이 많을 경우 디스크 I/O로 인해 성능 저하가 발생할 수 있음
3. redis.conf
1
2
3
4
5
6
7
8
9
10
11
# AOF 기능 활성화
appendonly yes
# 매 초마다 디스크에 로그 동기화 (기본값)
appendfsync everysec
# 모든 쓰기 명령마다 디스크에 동기화 (느리지만 최고 안전)
# appendfsync always
# 운영체제가 알아서 동기화 (가장 빠르지만 위험)
# appendfsync no
This post is licensed under CC BY 4.0 by the author.