오늘 배운 것
- 캐싱
- 캐싱 전략
- 캐싱 in Spring
캐싱이란?
Cache란?
캐싱은 컴퓨팅에서 데이터를 더 빠르게 접근할 수 있는 장소에 임시로 저장하여 시스템 성능을 향상시키는 기법이다.
더 빠르게 접근할 수 있는 장소가 Cache이다.
Cache는 본래 CPU 내부의 작은 영역으로, 정말 빈번히 접근하게 되는 데이터를 저장해두는 임시 기억 장치다.
데이터를 영속성을 위해 파일시스템(디스크)에 저장하고, 빠른 활용을 위해 메모리(RAM)에 저장한다.
그리고 정말 많이 사용되는 휘발성 데이터가 캐시에 저장된다.
Cache는 CPU에 딱 붙어 있어서 RAM을 오가는 속도보다 캐시의 데이터 가져오는 것이 더 빠르다.
캐싱 in WEB
캐시의 목적과 방식을 웹 개발에 적용했다.
빈번하게 접근하게 되는 데이터베이스의 데이터를 Redis 등의 인메모리 데이터베이스에 저장을 한다.
그래서 데이터를 조회하는데 걸리는 시간과 자원을 감소시키는 것이다.
서버에서 데이터 가져오는데 이 중 대부분은 많이 바뀌지 않는 데이터일 것이다.
이런 데이터들은 다음 요청에 서버에 다시 요청해서 가져오기 보다 브라우저 내부의 저장소에 저장해서 내부 저장소에서 가져오자는 것이다.
캐시의 내용을 웹 개발 분야에 적용해서 브라우저 캐시라는 개념이 생긴 것이다.
캐싱 전략
캐싱을 적용하는 것이 무조건 좋은 것은 아니다.
캐시에 필요한 데이터가 없으면 다시 DB로 가서 확인해야 하기 때문에 하나의 절차가 더 늘어난다.
그래서 캐시를 구현하고 사용할때는 해당 데이터가 얼마나 자주 사용될 것인가를 고려해야 한다.
캐시에다가 데이터를 언제 넣을 것인지, 캐시에 데이터가 있는지 없는지 이런 것을 구분하기 위해 용어가 존재한다 .
- 캐시 적중(Cache Hit):
- 캐시에 접근했을 때 찾고 있는 데이터가 있는 경우를 나타낸다.
- 캐시 누락(Cache Miss):
- 캐시에 접근했을 때 찾고 있는 데이터가 없는 경우를 나타낸다.
- 삭제 정책(Eviction Policy):
- 캐시에 공간 부족할때 어떻게 공간을 확보하는지에 대한 정책이다.
캐시를 구성할 때 hit 할 가능성은 최대한 높이고 miss할 가능성을 최대한 낮추는 것이 기본적인 전략이다.
Eviction policy는 어떤 상황에서 데이터를 캐시에서 비울 것인지에 대한 정책이다.
이런 것들을 잘 조합해서 어떻게 구성하면 된다를 모아둔 것이 캐싱 전략이다.
대표적인 캐싱 전략 3가지를 보자.
Cache-Aside 전략
개념
- 데이터를 조회할 때 항상 캐시를 먼저 확인하는 전략이다.
- 캐시에 데이터가 있으면 캐시에서 데이터를 가져온다.
- 캐시에 데이터가 없으면 DB에서 데이터를 가져온 뒤 캐시에 저장한다.
- Lazy Loading이라고도 한다.
장점
- 필요한 데이터만 캐시에 보관된다.
- 매우 단순한 전략이다.
- 첫번재 사용자를 제외하면 좋은 성능을 보장해 줄 수 있다.
단점
- 최초 조회할 때에는 캐시에 없지만 캐시를 확인해서 최초 요청은 상대적으로 오래 걸린다.
- 반드시 원본을 확인하지 않기 때문에 데이터가 최신이라는 보장이 없다.
- 따라서 적당한 시점마다 캐시 비워 다시 데이터 채워야 한다.
Write-Through 전략
개념
- 데이터를 작성할 때 항상 캐시에 작성하고 원본에도 작성하는 전략이다.
- 데이터 읽을 때에는 항상 캐시에서 가지고 오면 된다.
장점
- 캐시의 데이터 상태는 항상 최신 데이터임이 보장된다.
- 캐시가 오히려 DB보다 더 빠르게 업데이트 된다.
단점
- 자주 사용하지 않는 데이터도 캐시에 중복해서 작성하기 때문에 캐시에 자리를 차지한다.
- 무조건 캐시에 접근해서 데이터 작성해서 지연이 조금 생기고 시간이 오래 걸린다.
Write-behind 전략
개념
- 캐시에만 데이터 작성하고 일정 주기로 원본을 갱신하는 전략이다.
장점
- 데이터 쓰기가 잦은 상황에 DB의 부하를 줄일 수 있다.
단점
- 캐시의 데이터가 원본에 적용되기 전 문제 발생하면 데이터 소실의 위험성이 있다.
캐싱 in Spring
캐싱은 Spring 내부에서도 비교적 간단하게 구성할 수 있다.
Spring에 캐싱 관련 기능이 있어 쉽게 캐싱 동작할 수 있다.
메서드가 실행이 될 때 어떤 식으로 메서드 결과를 캐시할지 annoatation 기반으로 실행할 수 있다.
Redis만 추가해 놓으면 Redis를 바탕으로 캐싱을 구성할 수 있다.
캐싱 설정 (@EnableCaching)
- Spring에서 캐싱을 구성하는 것은 Spring 자체에 있는 기능이다.
- 캐싱 기능을 활성화 하고 설정하기 위해 Configuration을 만들어야 한다.
- Configuration을 만들어서 진행하면 되어서 CacheConfig를 만들어 진행한다.
- 따라서 CacheConfig 클래스에 @Configuration, @EnableCaching을 추가한다.
아래 글이 길어서 간단하게 설명하면 아래와 같다.
- @Configuration, @EnableCaching 붙은 클래스 작성
- @Bean 붙인 RedisCacheManager 생성하는 메서드 작성
- 파라미터로 RedisConnectionFactory 받기 (Redis 관련 연결 정보 구성)
- RedisCacheConfiguration 생성 (Redis 이용해 캐싱 구현 시 설정 정보 구성)
- RedisCacheManager를 RedisConnectionFactory + RedisCacheConfiguration으로 생성 후 반환
캐싱 기능을 구성하려면 @EnableCaching을 넣어줘야 한다.
@EnableCaching은 annotation 바탕으로 캐싱을 조절할 수 있게 해준다.
그리고 @EnableCaching 사용하려면 캐시를 관리하는 CacheManager 인터페이스의 구현체 Bean으로 등록되어야 한다.
그래서 @Configuaration과 @Bean 필요하다.
CacheManager의 구현체로 RedisCacheManager를 선택한 것이다.
RedisCacheManager는 Redis와 연결 정보가 구성되어야 해서 RedisConnectionFactory를 파라미터로 받아야 한다.
그리고 RedisCacheManager가 사용할 설정을 구성해야 한다.
RedisCacheConfiguration 클래스는 Redis를 이용해서 Spring Cache를 구현할 때 Redis 관련 설정을 모아두는 클래스다.
RedisCacheConfiguration 클래스가 가지는 속성들을 통해서 RedisCacheManager를 구성하는 것이다.
RedisCacheConfiguration에서 설정하는 속성들은 찾아보도록 하자.
// 나머지 import 문들
import static org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
// CacheManager로 진행해도 정상 동작
public RedisCacheManager cacheManager(
RedisConnectionFactory redisConnectionFactory
) {
// 설정 구성을 먼저 진행한다.
// Redis를 이용해서 Spring Cache를 사용할 때
// Redis 관련 설정을 모아두는 클래스
RedisCacheConfiguration configuration = RedisCacheConfiguration
.defaultCacheConfig()
// null을 캐싱 할것인지
.disableCachingNullValues()
// 기본 캐시 유지 시간 (Time To Live)
.entryTtl(Duration.ofSeconds(10))
// 캐시를 구분하는 접두사 설정
.computePrefixWith(CacheKeyPrefix.simple())
// 캐시에 저장할 값을 어떻게 직렬화 / 역직렬화 할것인지
.serializeValuesWith(
SerializationPair.fromSerializer(RedisSerializer.java())
);
return RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(configuration)
.build();
}
}
Redis 사용해서 RedisCacheManager를 만들어 Bean으로 등록한 것이다.
RedisCacheManager Bean으로 등록했다면 설정한 것을 바탕으로 캐싱 구성할 수 있다.
annotation 기반 선언형 캐싱 구현 가능해진 것이다.
이제 아래 annotation들을 사용해서 캐싱을 쉽게 구현할 수 있다.
아래 annotation들은 메서드에 붙이는 것으로 메서드의 결과를 가지고 캐싱하는 것이다.
Cache Aside 전략 캐싱 적용 (@Cacheable)
메서드에 @Cacheable annotation 붙이면 메서드의 결과는 캐시 가능하다는 것이다.
- anntation에 2가지 속성을 넣는데 cacheNames와 key이다.
- cacheNames에 작성하는 것은 Spring 내부에서 캐시를 구분하기 위해 붙이는 이름이다.
- key는 데이터를 구분하기 위해 사용할 값을 지정하는 것이다.
- key에는 SpEL (Spring Expression Language)를 사용한다.
- key에 args[0]은 첫번째 파라미터를 의미한다.
- key에 methodName을 적으면 메서드 명이 들어간다.
// cacheNames: 메서드로 인해서 만들어질 캐시를 지칭하는 이름
// key: 캐시에서 데이터를 구분하기 위해 활용할 값
@Cacheable(cacheNames = "itemCache", key = "args[0]")
public ItemDto readOne(Long id) {
log.info("Read One: {}", id);
return repository.findById(id)
.map(ItemDto::fromEntity)
.orElseThrow(()
-> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
---------------------------------------------------------------------------------
@Cacheable(cacheNames = "itemAllCache", key = "methodName")
public List<ItemDto> readAll() {
return repository.findAll()
.stream()
.map(ItemDto::fromEntity)
.toList();
}
캐시 생성되면 cacheNames::key 이런 이름으로 생성이 되어 있다.
저장이 된 데이터를 요청할 때 Redis에 key가 존재하는지 확인한다.
존재하면 데이터를 Redis에서 반환한다.
존재하지 않으면 RDB로 다시 가서 데이터를 반환한다.
Cache Aside 전략이다.
Redis에 캐싱되어 있다면 ReadOne 메서드는 호출되지 않고 바로 Redis에서 값을 가져간다.
Write Through 전략 캐싱 적용 (@CachePut)
Write Through 전략을 사용하려면 @CachePut이라는 annotation을 메서드에 붙여주면 된다.
annotation에 넣는 속성도 cacheNames와 key로 같다.
key에 #result.id 이렇게 되어 있는데 의미는 반환하는 것 = ItemDto의 id를 key로 넣겠다는 것이다.
#result 하면 반환하는 것을 지칭할 수 있다.
@CachePut(cacheNames = "itemCache", key = "#result.id")
public ItemDto create(ItemDto dto) {
return ItemDto.fromEntity(itemRepository.save(Item.builder()
.name(dto.getName())
.description(dto.getDescription())
.price(dto.getPrice())
.stock(dto.getStock())
.build()
));
}
@Cacheable은 Cache Aside 전략을 구현한 것으로 데이터를 캐시에서 발견한 경우 메서드 자체를 실행하지 않는다.
@CachePut은 항상 메서드를 실행하고 결과를 캐싱하는 것이다.
그래서 Write Through 전략을 구현한 것이다.
여기서 팁은 @Cacheable과 @CachePut의 캐시 이름 + key를 맞췄다는 점이다.
모두 itemCache::id로 구성되어 있다.
이렇게 되면 create를 하고 캐시에 저장이 된다.
그리고 바로 생성한 Item을 읽어오려고 하면 readOne 메서드가 아니라 캐시에서 데이터를 가져온다.
Write Behind 전략은 annotation으로 구현할 수 없다.
따라서 직접 구현해야 한다고 한다.
캐싱 삭제 (@CacheEvict)
update 할 때에도 캐시를 갱신해야 한다.
그런데 문제가 되는 것은 전체 목록을 캐시해 놓은 캐시이다.
update 되어서 이미 만들어진 데이터 캐시가 더이상 일치하지 않게 된다.
따라서 update 할 때에는 전체 목록 캐시를 지우고 싶은 것이다.
이럴 때 @CacheEvict를 사용하면 된다.
- 속성으로는 cacheNames, allEntires, key가 있다.
- cacheNames는 여기 있는 이름의 key를 가진 캐시 제거하라는 의미다.
- allEntries = true는 cacheNames의 이름을 가진 캐시에 있는 모든 데이터 제거하겠다는 의미다.
- key는 지정한 key의 캐시만 제거하겠다는 것이다.
@CachePut(cacheNames = "itemCache", key = "args[0]")
@CacheEvict(cacheNames = "itemAllCache", allEntries = true)
public ItemDto update(Long id, ItemDto dto) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
item.setName(dto.getName());
item.setDescription(dto.getDescription());
item.setPrice(dto.getPrice());
item.setStock(dto.getStock());
return ItemDto.fromEntity(itemRepository.save(item));
}
delete를 할 때에도 삭제하는 아이템의 캐시가 있는 경우 삭제해야 한다.
그리고 전체 목록의 캐시도 삭제를 해줘야 한다.
그러면 @CacheEvict를 2번 사용하면 되는데 메서드에 2개를 붙일 수 없다.
이럴 때에는 @Caching을 사용해서 2개의 @CacheEvict를 사용하면 된다.
// 삭제될 경우 단일 캐시, 전체 캐시 전부 초기화.
@Caching(evict = {
@CacheEvict(cacheNames = "storeCache", key = "args[0]"),
@CacheEvict(cacheNames = "storeAllCache", allEntries = true)
})
public void delete(Long id) {
storeRepository.deleteById(id);
}
'TIL' 카테고리의 다른 글
24.08.26 TIL - 발생 가능 장애 (1) | 2024.08.27 |
---|---|
24.08.23 TIL - 모니터링 (0) | 2024.08.26 |
24.08.21 TIL - session clustering (0) | 2024.08.23 |
24.08.20 TIL - Redis in Spring (0) | 2024.08.21 |
24.08.19 TIL - Redis (0) | 2024.08.20 |