[Redis] MySQL 조회 속도 vs Redis 조회 속도
* 개요
타 RDBMS보다 NoSQL인 redis의 속도가 훨씬 빠르다길래 궁금해서 한번 실습을 해보았다.
Framework는 Spring Boot를 사용, 더미 데이터는 100만 건을 넣었고, 간단한 게시글 조회로 실습을 진행하였다.
조회 속도 확인은 Postman을 통해 진행하였다.
* Repository 구현
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
Page<Board> findAllByOrderByCreatedAtDesc(Pageable pageable);
}
ORM은 JPA를 사용하였고, 최신 생성 시간 순으로 게시글을 조회할 수 있게 하였다.
* Service 구현 - MySQL 사용
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
public List<Board> getBoards(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Board> pageOfBoards = boardRepository.findAllByOrderByCreatedAtDesc(pageable);
return pageOfBoards.getContent();
}
}
* Controller 구현
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/boards")
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@GetMapping()
public List<Board> getBoards(
@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size
) {
return boardService.getBoards(page, size);
}
}
1페이지의 10개의 게시글을 불러오도록 요청을 처리한다.
* 조회 속도 확인 - MySQL
평균적으로 700~800 ms가 소요되는 것을 확인하였다. 그렇다면 redis의 조회 속도는 어떨까?
* RedisConfig 구현
Redis를 사용하기 위해 config 패키지를 만든 후 RedisConfig를 만들었다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
}
application.yml 파일에 설정해놨던 redis의 host와 port를 매핑해준다.
그 후 Lettuce라는 라이브러리를 활용해 Redis 연결을 관리하는 객체를 생성하고 Redis 서버에 대한 정보(host, port)를 설정한다.
* RedisCacheConfig 구현
Redis 캐싱을 위한 Config도 만들어준다.
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager boardCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<Object>(Object.class)
)
)
.entryTtl(Duration.ofMinutes(1L));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
RedisCacheConfiguration을 통해 구현을 하는데, redis에 key를 저장할 때 String 타입으로 직렬화해서 저장한다.
redis에 value를 저장할 때는 JSON으로 직렬화해서 저장한다.
마지막으로 데이터의 만료기간(TTL)을 설정해준다. 간단한 실습이기에 1분으로 설정하였다.
* Service 구현 - Redis 사용
Redis를 사용할 수 있게 service 로직을 수정한다.
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
@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> pageOfBoards = boardRepository.findAllByOrderByCreatedAtDesc(pageable);
return pageOfBoards.getContent();
}
}
@Cacheable 어노테이션을 붙이면 Cache Aside(Look Aside) 전략을 사용한다.
- 해당 메서드로 요청이 들어오면 redis에서 데이터가 존재하는지 확인한다.
- 데이터가 존재한다면 redis의 데이터를 조회해서 바로 응답을 할 것이고, 데이터가 없다면 메서드 내부의 로직을 실행시킨 뒤, return 값으로 응답한다.
- 그리고 return 값을 redis에 저장한다.
- cacheNames : 캐시 이름 설정
- key : redis에 저장할 key 이름 설정
- cacheManager : cacheManager의 Bean 이름 지정
이제 Redis의 조회 속도를 확인해보자.
* 조회 속도 확인 - Redis
처음에 조회를 해보니 No cache entry라고 나온다.
redis에 데이터가 존재하지 않는다는 말이다.
그 후 board에 대한 응답을 처리한 뒤 redis에 데이터를 저장했을 것이다. 로그를 확인해보면,
redis에 데이터를 저장했음을 확인할 수 있다.
redis-cli를 실행시킨 뒤 redis에 데이터가 있는지 확인해보자.
keys * 명령어를 확인해보니 분명히 존재한다. 이제 TTL이 만료되기 전에 빨리 Postman에서 데이터를 조회해보자.
10 ms가 소요된다. MySQL과 Redis의 조회 속도를 비교해보면, 700 ms -> 10 ms 단축됐음을 알 수 있다.
그럼 항상 redis를 쓰는게 좋을까? 그건 아니다.
TTL을 통해 데이터가 주기적으로 삭제되는데, 자주 바뀌는 민감한 데이터에 대해서는 사용하기 어려울 것이다.
따라서 자주 조회되면서 잘 변하지 않는 데이터에 대해 redis를 쓰는게 효율적이며, 정책에 따라 TTL 설정을 잘 해야 한다!