728x90
반응형
SMALL
학습내용
- Redis를 활용한 캐싱전략
- Redis 성능 최적화
- 데이터 일관성 문제 해결
학습정리
1. Redis를 활용한 캐싱전략
@Slf4j
@Service
@RequiredArgsConstructor
public class CategoryService {
//상수로 redis키 설정
//전역으로 쓰는 상수값은 constants 패키지에서 관리
private static final String CACHE_KEY_CATEGORY_STRUCT = "category_struct";
private static final String CACHE_KEY_PRODUCT_COUNT = "product_count";
private final RedisService redisService;
private final CategoryRepository categoryRepository;
private final CategoryQueryRepository categoryQueryRepository;
...생략 (기본적인 CRUD)
//Cache-aside 패턴 -> 조회시 캐시먼저 확인 후 없으면 db에서 가져오고 캐시저장
public List<CategoryProductCountResponse> productCounts(CategoryProductCountRequest search) {
//dto 해시코드와 결합해여 키 설정
String cacheKey = CACHE_KEY_PRODUCT_COUNT + search.hashCode();
//캐시에서 데이터 확인
List<CategoryProductCountResponse> cachedResponse = redisService.getObject(cacheKey,
new TypeReference<List<CategoryProductCountResponse>>() {
});
//해당 값이 비어있지 않으면 반환
if (!ObjectUtils.isEmpty(cachedResponse)) {
return cachedResponse;
}
//없을 경우 db에서 조회 후 캐시 저장
List<CategoryProductCountResponse> responses = categoryQueryRepository.productCount(search);
if (!ObjectUtils.isEmpty(responses)) {
redisService.saveObject(cacheKey, responses);
}
return responses;
}
//카테고리 계층형 구조 가져오는 로직 Map을 활용
public List<CategoryResponse> findStruct() {
List<Category> categories = categoryRepository.findAll();
Map<Long, CategoryResponse> categoriesResponseMap = new HashMap<>();
for (Category category : categories) {
CategoryResponse response = CategoryResponse.builder()
.name(category.getName())
.children(new ArrayList<>())
.build();
categoriesResponseMap.put(category.getId(), response);
}
List<CategoryResponse> rootCategories = new ArrayList<>();
for (Category category : categories) {
CategoryResponse categoryResponse = categoriesResponseMap.get(category.getId());
if (ObjectUtils.isEmpty(category.getParent())) {
rootCategories.add(categoryResponse);
} else {
CategoryResponse parentCategoryResponse = categoriesResponseMap.get(
category.getParent().getId());
parentCategoryResponse.getChildren().add(categoryResponse);
}
}
return rootCategories;
}
//Cache-aside 패턴
public List<CategoryResponse> findCategoryStructCacheAside() {
List<CategoryResponse> cacheCategories = redisService.getObject(CACHE_KEY_CATEGORY_STRUCT,
new TypeReference<List<CategoryResponse>>() {
});
if (!ObjectUtils.isEmpty(cacheCategories)) {
return cacheCategories;
}
List<CategoryResponse> rootCategories = findStruct();
if (!ObjectUtils.isEmpty(rootCategories)) {
redisService.saveObject(CACHE_KEY_CATEGORY_STRUCT, rootCategories);
}
return rootCategories;
}
//write-through는 DB,cache 동시 업데이트
public Boolean saveWriteThrough(CategoryRequest request) {
Category category = Category.builder()
.name(request.getName())
.build();
categoryRepository.save(category);
//위에 save 한 값이 db에 저장되고
//updateCache()를 통해 저장된 값을 가져와 캐시에 저장
updateCache();
return true;
}
public void updateCache() {
//캐시가 저장 안됐다고 에러처리하기 애매해서 try catch로 잡음
try {
//새로 업데이트된 db에서 새로 조회
List<CategoryResponse> rootCategories = findStruct();
//redis에 해당키 업데이트
redisService.saveObject(CACHE_KEY_CATEGORY_STRUCT, rootCategories);
} catch (Exception e) {
log.error("Error updating cache: {}", CACHE_KEY_CATEGORY_STRUCT, e);
}
}
//write-back : 캐시먼저 저장 후에 DB 비동기로 저장
public Boolean saveWriteBack(CategoryRequest request) {
List<CategoryResponse> categories = redisService.getObject(CACHE_KEY_CATEGORY_STRUCT,
new TypeReference<List<CategoryResponse>>() {
});
if (ObjectUtils.isEmpty(categories)) {
//없으면 초기화 후에
categories = new ArrayList<>();
}
CategoryResponse newCategory = CategoryResponse.builder()
.name(request.getName())
.children(new ArrayList<>())
.build();
categories.add(newCategory);
redisService.saveObject(CACHE_KEY_CATEGORY_STRUCT, categories, 3600);
saveToDatabaseAsync(request);
return true;
}
//비동기 메서드는 Async 붙여주기
@Async
public void saveToDatabaseAsync(CategoryRequest request) {
try {
categoryRepository.save(
Category.builder()
.name(request.getName())
.build()
);
} catch (Exception e) {
//TODO : 캐시초기화나 예외 발생시 문제 해결 로직 필요
log.error("Error updating data : {}", e.getMessage(), e);
}
}
}
2. Redis 성능 최적화
만료시간 설정(TTL) : 기본적으로 해당 방식으로 성능 최적화 진행
기존 RedisService 수정
@Slf4j @Service @RequiredArgsConstructor public class RedisService { private final Jedis jedis; private final ObjectMapper objectMapper; public <T> void saveObject(String key, T object) { try { String jsonValue = objectMapper.writeValueAsString(object); jedis.set(key, jsonValue); } catch (JsonProcessingException e) { log.error("[RedisService] saveObject#{}", e.getMessage()); } } //오버로딩으로 TTL설정 메서드추가 public <T> void saveObject(String key, T object, Integer ttlInSeconds) { try { String jsonValue = objectMapper.writeValueAsString(object); jedis.setex(key, ttlInSeconds, jsonValue); } catch (JsonProcessingException e) { log.error("[RedisService] saveObject#{}", e.getMessage()); } } //get 할때 반환 받고 싶은 타입을 같이 인자로 넣음 public <T> T getObject(String key, TypeReference<T> type) { try { String jsonValue = jedis.get(key); return ObjectUtils.isEmpty(jsonValue) ? null : objectMapper.readValue(jsonValue, type); } catch (Exception e) { log.error("[RedisService] getObject #{}", e.getMessage()); return null; } } }
LRU (Least Recently Used) 및 LFU (Least Frequently Used) 정책
- LRU : 가장 오랫동안 사용 되지 않은 데이터를 우선적으로 제거
- LFU : 사용 빈도가 가장 낮은 데이터를 제거
- 설정방법 : maxmemory-policy 설정을 통해 정책 변경
CONFIG SET maxmemory-policy allkeys-lru # 모든 키에 대해 LRU 정책 사용 CONFIG SET maxmemory-policy volatile-lru # 만료 시간이 설정된 키에만 LRU 사용 CONFIG SET maxmemory-policy allkeys-lfu # 모든 키에 대해 LFU 정책 사용
3. 데이터 일관성 문제 해결
Cache-Aside 와 Write-through를 함께 사용
- 동작방식
- 데이터 조회 -> Cache-Aside : 캐시 있으면 사용 없으면 데이터베이스에서 읽어서 캐시 저장
- 데이터 쓰기 -> Write-through : 캐시와 데이터베이스 동시에 데이터를 갱신
- 동작방식
Cache Eviction :
동작 방식: 데이터가 업데이트 될 때, 해당 키의 캐시 데이터를 제거 후 새로 데이터 로드시 캐싱이 저장되도록 처리 방식
예시
public void eviction(String key){ //해당 키에 대한 캐시데이터 삭제 redisService.delete(key); ...이후 데이터베이스 업데이트 로직 }
TTL 설정
- 캐시 데이터 유효기간 설정
- 데이터 갱신이 많을 경우 만료시간을 적게 해당 도메인에 맞게 처리
- 데이터 갱신이 적을 경우 만료시간을 길게 해당 도메인에 맞게 처리
- 캐시 데이터 유효기간 설정
참고자료
- 캐싱전략 : 4_2.인메모리 저장소 및 캐싱 전략 개요
- Redis : 4_3.Redis 데이터 타입 활용
ps. 앞에서 캐싱전략과 Redis에 대해서 배웠었는데 당시에 배우면서 이걸 어떤식으로 사용하나 싶었는데
이번 시간에 배우면서 큰 흐름을 깨닫게 된 것 같아서 보람찬 시간이었습니다. :)
728x90
반응형
LIST
'TIL' 카테고리의 다른 글
5_1.인덱스 설계 및 활용 (0) | 2025.01.20 |
---|---|
4_5.리더보드와 Sorted Set 실습 (0) | 2025.01.17 |
4_3.Redis 데이터 타입 활용 (0) | 2025.01.16 |
4_2.인메모리 저장소 및 캐싱 전략 개요 (0) | 2025.01.14 |
4_1.HTTP Session과 Session Clustering (0) | 2025.01.13 |