이전 포스팅에서 스프링부트의 캐싱을 사용방법을 알아봤다.
이번 포스팅에서는 페이징 처리에 캐시를 도입하기 위한 방법을 살펴본다.
어느 부분을 캐싱할래?
캐시는 key-value
로 캐시 메모리에 저장된다.
페이징 조회에서 사용되는 key는 Pageable
에서 사용되는 sort
,size
, offset
임으로, 특정 레코드의 데이터가 변경됐을 때, 이 데이터가 포함된 캐시를 갱신할 수 없다.
예시를 들어보자.
// 1. id 를 이용해 특정 사용자의 이름을 수정하는 메소드
@Caching(
put = @CachePut(key = "#id"),
evict = @CacheEvict("'all'")
)
@Transactional(readOnly = false)
public Member changeName(final Long id, final String name) {
final Member member = memberRepository.findById(id).orElseThrow();
member.setName(name);
return member;
}
}
위와 같이 특정 사용자의 이름을 수정하는 로직이 존재한다.
// 2. 페이지 기반으로 모든 Member 조회
@Cacheable(key = "#pageable.offset + '-' + #pageable.pageSize + '-' + #pageable.sort")
public Page<Member> queryByPage(final Pageable pageable) {
return memberRepository.findAll(pageable);
}
이 경우, (1)에서 수정되는 사용자의 id로는 캐싱된 페이지의 key를 알 수 없음으로, 해당 캐시를 갱신할 수 없다.
따라서, 다음과 같은 방법으로 대안한다.
1. 모든 페이징 요청 캐싱 + 갱신(수정, 삭제, 추가) 시 초기화
사용자에게 항상 최신정보를 전달할 수 있다.
다만, 수정과 삭제가 빈번한 도메인의 경우 전체 캐시 메모리가 빈번히 Evict됨에 따라, 쿼리가 자주 발생하게 됨으로, 캐시메모리를 도입한 의의가 없어진다 볼 수 있다.
2. 모든 페이지 요청 캐싱 + TTL 최소화
쿼리를 획기적으로 줄일 수 있다.
다만, 사용할 수 없는 데이터가 쿼리 결과에 일정시간 존재함에 따라, 사용자 경험을 떨어뜨릴 수 있다. (최신 정보 전달 불가)
@Configuration
@EnableCaching
public class CacheConfig {
@Autowired
RedisProperties redisProperties;
@Bean
RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(
redisProperties.getHost(),
redisProperties.getPort()
);
}
RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
}
@Bean
CacheManager cacheManager() {
return RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration())
// Page Cache를 위한 로직 설정 TTL 10초 설정
.withCacheConfiguration("pageCache", redisCacheConfiguration().entryTtl(Duration.ofSeconds(10L)))
.build();
}
}
위와 같이 pageCache
를 설정한다.
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "pageCache")
public class MemberQueryService {
private final MemberRepository memberRepository;
@Cacheable(key = "#pageable.offset + '-' + #pageable.pageSize + '-' + #pageable.sort")
public PageResponse<Member> queryByPage(final Pageable pageable) {
return mapToPage(memberRepository.findAll(pageable));
}
private PageResponse<Member> mapToPage(final Page<Member> members) {
return new PageResponse<Member>(members.getContent(), members.getSize(), members.getTotalPages());
}
}
pageCache
를 사용하여 페이징 처리하며, 10초마다 이를 파기한다.
특정 데이터가 수정될 떄, 해당 데이터가 포함된 페이징 캐시만 수정할 수 있다면 좋겠지만, 페이징 쿼리의 특성 상, 조회 조건이 여럿으로 걸릴 수 있음으로이에 대한 모든 캐시를 업데이트하는 것은 추가적인 오버헤드가 발생한다.
사용자가 많고, 수정 및 추가 이벤트가 빈번한 도메인의 경우 TTL을 적절히 조정하며 가장 적절한 상수를 찾는것이 좋아보인다.
'SpringBoot' 카테고리의 다른 글
Spring boot - @Transactional 전파 주의 사항 (0) | 2025.01.13 |
---|---|
Spring boot - Cache (0) | 2025.01.07 |
[SpringBoot] 스프링의 에러처리 탐구 (0) | 2024.01.17 |
[Spring] Spring Security 인증 구성 (0) | 2024.01.02 |
[Spring] 다수의 SecurityFilterChain 구성 방법 (0) | 2024.01.01 |