데이터 캐싱의 개념에 대해 알아보고, Redis의 캐싱 기능을 활용하여
조회 성능을 개선하는 예제를 포함한 포스팅입니다.
✅ 캐싱이란?
캐시(Cache)는 데이터나 값을 저장하는 임시 저장소로, 데이터에 더 빠르고 효율적으로 엑세스할 수 있게 해준다.
캐시는 주로 Redis와 같은 In-memory 저장소를 사용하기에 일반적인 RDB에 비해 엑세스 속도가 훨씬 빠르다.
캐시가 없는 경우 일반적으로 클라이언트와 서버간 데이터 엑세스의 흐름은 다음과 같다.
동일한 데이터에 반복적으로 접근하는 상황에서, 서버는 DB에 매 요청마다 쿼리를 전송하게 된다.
여기서 캐시가 추가되게 되면, 서버 API는 DB에 특정 데이터에 대한 쿼리를 보내기전에 해당 데이터가
캐시에 존재하는지 먼저 확인한다. 캐시에 데이터가 있다면 DB를 거치지 않고(쿼리를 보내지 않고) 캐시에서
데이터를 가져오고, 캐시에 데이터가 없다면 DB에 쿼리를 전송하고 해당 데이터를 캐시에 저장한다.
즉, 값이 잘 변하지 않는 같은 데이터에 반복적인 엑세스가 발생하는 상황(인증 세션 등)에서 캐싱을 적용하면
조회 속도에 큰 성능 개선을 가져올 수 있다.
✅ Redis를 활용한 캐싱 적용
본 포스팅에선 Redis의 캐시 매니저(CacheManager)를 활용하여 캐싱을 구현해 볼 것이다.
📌 의존성 및 properties 설정
(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
(application.properties)
spring.data.redis.port=6379
spring.data.redis.host=localhost
Redis를 사용하기 위해 의존성 및 properties 설정을 해준다.
📌 RedisConfig
@EnableRedisRepositories // redis 활성화
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
// 연결정보
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
// 직렬화 / 역직렬화
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
// 캐시 매니저
@Bean
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(
redisConnectionFactory());
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
.disableCachingNullValues()
.entryTtl(Duration.ofMinutes(10L));
builder.cacheDefaults(configuration);
return builder.build();
}
}
Redis를 사용하여 캐싱을 설정하기 위한 Config 클래스이다.
RedisConnectionFactory
- redisConnectionFactory(): Redis 서버에 연결하기 위한 RedisConnectionFactory를 설정
- RedisStandaloneConfiguration을 사용하여 Redis 서버의 호스트와 포트를 설정
- LettuceConnectionFactory를 사용하여 Redis 연결 팩토리를 만듦
RedisTemplate
- redisTemplate(): Redis 서버와의 상호작용을 위한 RedisTemplate을 설정
- RedisTemplate<String, Object>를 생성하고, 연결 팩토리를 설정
- StringRedisSerializer를 사용하여 키를 직렬화
- GenericJackson2JsonRedisSerializer를 사용하여 값을 직렬화
CacheManager
- cacheManager(): Spring 캐시 매니저를 설정
- RedisCacheManager.RedisCacheManagerBuilder를 사용하여 Redis 연결 팩토리로부터 캐시 매니저를 생성
- RedisCacheConfiguration을 통해 캐시 설정을 구성
- 값을 직렬화하기 위해 GenericJackson2JsonRedisSerializer를 사용
- disableCachingNullValues()를 통해 null 값을 캐시하지 않도록 설정
- 캐시 항목의 TTL(Time To Live)을 10분으로 설정
- 최종적으로 설정된 캐시 매니저를 반환
이렇게 Config 설정을 마치고 나면 원하는 서비스 API에 애너테이션을 통해 캐싱을 설정할 수 있다.
📌 서비스 레이어에 캐싱 적용
위 설정이 끝났다면 캐싱을 적용하는 것은 간단하다. 기존 서비스 API에 애너테이션을 하나 붙여주면 데이터 캐싱이 적용된다.
@Transactional(readOnly = true)
@Cacheable(value = "board", key = "#user.id", cacheManager = "cacheManager", unless = "#result == null")
public List<BoardDto> getBoards(User user) {
return boardRepository.getBoards(user);
}
위 코드는 특정 유저의 보드 데이터를 조회하는 서비스 API다.
@Cacheable 애너테이션을 통해 해당 데이터를 Redis 저장소(캐시)에 저장할 수 있다.
캐시에 데이터가 없을 땐 DB에 쿼리를 전송하게 되지만 캐시 데이터가 있는 경우엔 해당 데이터를 반환하게 된다.
한번 DB를 통해 보드 데이터 조회가 발생하게 되면, 위와 같이 애너테이션의 파라미터에 설정한대로 캐시 데이터의 키에는 User 아이디(1), 벨류 값에는 해당 보드에 대한 정보가 저장되게 된다.
이렇게 캐시에 데이터가 저장되고 나면 이후의 데이터 요청에는 DB를 조회하지 않고 캐시에 저장된 위 정보를 반환하게 된다.
📌 캐시 무효화
앞서 설명했듯이 캐싱은 값이 변하지 않는 동일한 데이터에 접근할 때 적용해야 한다.
따라서 만약 캐시에 저장된 데이터의 원본 데이터(DB 데이터)에 변동이 생긴다면 캐시에 저장된 값은
무효화를 해야 한다. 이러한 캐시 무효화 역시 마찬가지로 애너테이션을 활용해 간단하게 구현 가능하다.
@Transactional
@CacheEvict(value = "board", key = "#user.id", allEntries = true)
public Long updateBoard(Long boardId, BoardRequest boardRequest, User user) {
Board board = getBoardById(boardId);
checkBoardStateIsTrue(board);
validateBoardOwner(user, board);
updateBoardAttributes(board, boardRequest);
return board.getId();
}
위 코드는 앞서 캐싱을 적용한 보드 서비스 레이어에서, 보드의 값을 변경(업데이트)하는 서비스 API이다.
보드의 데이터가 캐시에 저장되어 있는데 해당 보드의 값이 변경된다면 캐시의 무효화가 필요하다.
@CacheEvict 애너테이션을 활용하면 간단하게 구현할 수 있다.
@Cacheable 애너테이션과 반대로 캐시에 존재하는 해당 키-벨류 쌍의 캐시 데이터를 제거하는 애너테이션이다.
이러한 캐시 무효화는 해당 데이터의 수정 API 뿐만 아니라 create, delete 등의 API에도 적용해야 할 수 있다.
✅ 조회 속도 비교
📌 캐싱 적용 전
캐싱을 적용하지 않고 1개의 보드 데이터를 조회했을 때 걸린 속도이다. 42ms 가 나왔다.
📌 캐싱 적용 후
캐싱을 적용한 이후의 조회 속도이다. 11ms로 캐싱을 적용하기 전보다 약 4배가량 조회 속도가 개선된 모습이다.
이 테스트에선 조회 데이터가 1개뿐이지만 데이터의 양이 많아질수록 이러한 속도 차이는 더욱 선명하게 드러날 것이다.
'TIL' 카테고리의 다른 글
[AWS ECS와 Fargate를 활용한 Auto-Scalable 아키텍처 구현] Intro (1) | 2024.04.01 |
---|---|
[Spring][Redis] RedisTemplate을 활용한 캐싱 처리 (0) | 2024.03.29 |
[Spring][DB] 데이터의 순서 컬럼 정렬 전략 (0) | 2024.03.22 |
JWT의 무상태성 (+쿼리 최적화) (0) | 2024.03.08 |
[Spring] CRUD 기능 구현 중 알게 된 Dto를 사용해야 하는 이유 (0) | 2024.02.26 |