Redis 를 활용해 조회 성능 개선하기 - 1

연어·2024년 10월 31일
0

dev

목록 보기
2/6
post-thumbnail

Redis 를 활용해 데이터를 캐싱하여 조회 성능을 개선해보자.

캐시(Cache)

원본 저장소(Database) 보다 빠르게 가져올 수 있는 임시 데이터 저장소

캐싱(Caching)

캐시(임시 데이터 저장소)에 접근해 데이터를 빠르게 가져오는 방식

데이터 캐싱 전략

Cache Aside

데이터를 조회할 때 Redis 를 먼저 쳐서 데이터를 조회하고, 조회하고자 하는 데이터가 저장되어 있지 않으면 DB 에서 조회해 오는 방식

Write Around

데이터를 저장할 때 Redis 에 반영하지 않고 DB 에만 데이터를 저장

문제점

Cache Aside 는 조회에 초점을 맞추고, Write Around 는 저장에 초점을 맞추고 있어 두 전략을 같이 사용하는데, 이 때 문제가 될 수 있는 점이 2가지가 있다.

  • DB와 Redis 에 저장된 데이터가 다를 수 있다. 즉 데이터 일관성이 보장되지 않을 수 있다.
  • 캐시 저장 공간은 DB(Disk) 보다 작다.

데이터 동기화를 위해 DB 수정 사항이 있을 때마다 Redis 까지 반영하게 되면 성능 부하가 커지게 되기 때문에, 캐싱 서버를 활용할 때 이러한 문제를 해결하기 위해서 아래와 같은 케이스의 데이터를 기준으로 활용하는 것이 좋다.

  • 자주 조회되는 데이터
  • 잘 변하지 않는 데이터
  • 실시간으로 변경이 반영되지 않아도 될 데이터

또한 Redis 의 TTL 기능을 활용해서, 적절한 주기로 데이터를 동기화할 수 있도록 한다.
데이터가 만료되면 Cache Miss 가 발생하기 때문에 새롭게 DB 에서 조회한 후에 Redis 에 저장한다.
또 자주 사용하지 않는 데이터는 TTL 에 의해 자동으로 삭제될 것이기 때문에 저장 공간에 대한 여유도 확보할 수 있다.

Spring Boot + Redis

프로젝트 설정

  • Gradle
  • Java 17
  • Spring Boot 3.3.5
  • MySQL version 8.* 이상
  • Dependency
    Spring Boot DevTools Spring Web Spring Data JPA MySQL Driver

조회 성능을 테스트할 수 있는 간단한 조회 API 를 구현했다.
해당 API 에 캐싱을 적용할 수 있도록 Redis 설정을 추가한다.

  • RedisConfig
  • RedisCacheConfig

RedisConfig

Redis 연결을 설정한다.
spring data redis 에서 제공하는 Lettuce 라이브러리를 활용해 Redis 연결을 관리하는 Bean 을 등록한다.

@Configuration
public class RedisConfig {
	@Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;
    
    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
		    // Redis 서버의 호스트, 포트 설정
		    return new LettuceConnectionFactory(host, port);
	    }
}

RedisCacheConfig

Spring 의 캐싱 기능을 활성화하고 Redis 를 캐시 저장소로 사용하기 위한 추가 설정을 작성한다.
RedisCacheConfiguration 을 통해 캐시 저장 방식과 TTL 설정을 추가했다.
정의된 캐시 설정을 적용한 RedisCacheManager 빈을 등록해 board 도메인의 캐싱을 위한 boardCacheManager 을 생성한다.

@Configuration
@EnableCaching // Spring Boot 의 캐싱 활성화
public class RedisCacheConfig {
	
	@Bean
	public CacheManager boardCacheManager(RedisConnectionFactory redisConnectionFactory) {
			RedisCacheConfiguration redisCacheConfiguration = 
					RedisCacheConfiguration
                        .defaultCacheConfig()
                        .serializeKeysWith( // Redis 에 Key 를 저장할 때 String 으로 직렬화해 저장
                                RedisSerializationContext.SerializationPair.fromSerializer(
                                        new StringRedisSerializer()
                                ))
                        .serializeValuesWith( // Redis 에 Value 를 저장할 때 Json 으로 직렬화해 저장
                                RedisSerializationContext.SerializationPair.fromSerializer(
                                        new Jackson2JsonRedisSerializer<Object>(Object.class)
                                ))
                        .entryTtl(Duration.ofMinutes(1)); // TTL 설정
         
	    return RedisCacheManager
						    .RedisCacheManagerBuilder
						    .fromConnectionFactory(redisConnectionFactory)
						    .cacheDefaults(redisCacheConfiguration)
						    .build();
		}
}

BoardService

Spring 의 @Cacheable 을 사용하면 Cache Aside 전략을 적용할 수 있다.
예를 들어 1페이지에서 10개의 board 를 조회하는 요청이 오면 Redis 에 boards:page:1:size:10 key 로 저장된 데이터가 있는지 확인하고, 데이터가 없다면 내부 로직을 수행한다. Redis 에 저장되어 있을 경우 메서드를 실행하지 않고 Redis 에서 해당 value 를 바로 반환한다.

@Cacheable(cacheNames = "getBoards", key = "'boards:page:' + #page + ':size:' + #size", cacheManager = "boardCacheManager")
    public List<Board> getBoards(int page, int size) {
        Pageable pageable = PageRequest.of(page - 1, size);
        Page<Board> boards = boardRepository.findAllByOrderByCreatedAtDesc(pageable);
        return boards.getContent();
    }

결과

첫번째 요청

애플리케이션을 실행하고 첫 요청은 Redis 에 아무 데이터도 저장되어 있지 않기 때문에 아래와 같은 로그를 확인할 수 있다. Redis 에 데이터가 없어 DB select 후 캐싱되었다.

두번째 요청

첫번째 요청에서 Cache miss 가 발생했기 때문에 두번째 요청 시에는 아래와 같은 로그를 확인할 수 있다.
Redis 에서 boards:page:1:size:10 key 로 저장된 데이터를 조회했다.

redis-cli 로 데이터 확인

Redis 에 정상적으로 데이터가 저장된 것을 확인할 수 있다.

캐싱 전 후 성능 비교

첫번째 요청과 두번째 요청의 속도 차이를 비교해보자.
postman 으로 캐싱 전후의 수치를 확인할 수 있다.

첫번째 요청 - 286ms 소요

두번째 요청 - 6ms 소요

학습에 참고한 강의

https://inf.run/Pupon

profile
끄적이는 개발자

0개의 댓글