성능 향상을 위한 Redis 적용

JeongMin·2024년 6월 27일
0
post-thumbnail

왜 Redis를 사용해야 할까?

Cache

캐시의 사전적 정의는 데이터를 미리 복사해 놓는 임시 장소를 의미합니다.

데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 빠른 속도로 데이터에 접근할 수 있습니다.

현재 프로젝트에서 MySQL 데이터베이스를 사용하고 있는데, MySQL은 데이터를 디스크에 저장합니다. 디스크 I/O는 메모리 접근보다 훨씬 느리기 때문에 조회 성능을 개선하기 위해 캐시를 사용할 수 있습니다.

캐시를 사용하여 클라이언트가 동일한 읽기 요청에 대해 데이터베이스를 매번 접근하지 않고, 메모리에 저장된 사본 데이터를 가져와 성능 개선을 할 수 있습니다.

많은 사용자가 이용하는 서비스라면 캐시를 도입해 병목 현상을 개선할 수 있어 캐시를 도입하는 것이 좋다고 생각합니다.


Local Cache vs Global Cache

캐시 관리 전략을 선택할 때, 캐시 데이터를 스토리지가 서버 안에 소유하고 있을지, 외부 서버에 캐시 저장소를 따로 둘지 선택해야 합니다.

특징Local CacheGlobal Cache
범위특정 서버/애플리케이션 인스턴스 내여러 서버/애플리케이션 인스턴스 간 공유
속도매우 빠름네트워크 지연 발생 가능
운영단순함복잡함
일관성인스턴스 간 동기화 어려움일관성 보장
확장성제한적좋음
사용 사례애플리케이션 내부 캐싱분산 환경의 데이터 캐싱

Local Cache는 캐시 데이터를 서버 메모리 상에 두기 때문에 속도가 빠릅니다. 외부에 캐시 데이터를 두게 될 경우, 데이터를 가저오는 과정에서 발생하는 오버헤드가 발생할 수 있습니다.

현재 진행중인 프로젝트는 분산 서버로 구성될 예정이고 프로젝트 내에 데이터의 일관성을 고려해야 하는 부분도 있어 Global Cache를 적용할 것입니다.


Memcached vs Redis

Global Cache의 종류로는 MemcachedRedis가 있습니다.

MemcachedRedis 모두 In Memory 저장소 이며 Key-Value 저장방식을 지니고 있습니다.
또한, 무료 오픈소스이고 높은 응답 속도를 보여주기 때문에 대용량 트래픽을 고려하는 프로젝트에 적용하기 좋습니다.

특징MemcachedRedis
데이터구조문자열문자열, List, Set, Hash 등
성능멀티 스레드, 높은 성능단일 스레드, 비동기 I/O
영속성지원 X지원(RDB, AOF)
복제 및 고가용성지원 X (추가 설정 필요)마스터-슬레이브 복제, Multi-Master Replication 방식 지원
사용 사례웹 페이지, 데이터베이스 캐싱다양한 데이터 구조 캐싱, 메시지 브로커, 실시간 분석 등

RedisMemcached에 비해 쓰기 성능은 떨어집니다.
하지만 다양한 데이터 타입 지원세션 저장소, 분산 락과 같은 다양한 기능을 활용할 수 있기 때문에 Redis를 활용할 것입니다.


Spring Boot 기반 프로젝트에 Redis 적용

  • 레디스가 설치되어 있다는 가정하에 진행
@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}
@EnableCaching
@Configuration
public class RedisCacheConfig {

    @Bean
    public CacheManager rcm(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration()
                .entryTtl(Duration.ofSeconds(120));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(cf)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }

    private RedisCacheConfiguration generateCacheConfiguration() {
        PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator
                .builder()
                .allowIfSubType(Object.class)
                .build();

        ObjectMapper objectMapper = new ObjectMapper();

        JavaTimeModule javaTimeModule = new JavaTimeModule();

        objectMapper.registerModule(javaTimeModule);
        objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);

        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
    }
}
@SpringBootApplication
@EnableCaching
public class MyBoardApplication {

	public static void main(String[] args) {
		SpringApplication.run(MyBoardApplication.class, args);
	}

}
# yml
spring:
  redis:
    host: #서버 호스트
    port: 6379

RedisConfig 클래스를 통해 Redis 서버와 연결을 설정하고 Redis 데이터 처리를 위한 템플릿을 설정합니다.

RedisCacheConfig 클래스에서 Redis 캐시 매니저를 설정하고 ObjectMapper를 이용하여 Java 객체의 직렬화와 역직렬화를 설정했습니다. 또한, DTOLocalDateTime 타입을 String 형태로 변환하기 위한 과정입니다.


@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class BoardService {

	...
    
	@Cacheable(value = "boardByIdCache", key = "#boardId", cacheManager = "rcm")
    public BoardTotalInfoResponse findBoard(final Long boardId) {
    
		...
    }
    
    @Cacheable(value = "pagedBoardCache", key = "#pageable", cacheManager = "rcm")
    public List<BoardTotalInfoResponse> findLimitedBoardList(Pageable pageable) {
    
    	...
    }
}

캐시를 적용할 곳에 @Cacheable을 사용하고, 캐시를 삭제해야 한다면 @CacheEvict를 사용합니다.

value : 캐시의 이름을 지정함
key : 캐시에 저장할 때, 사용할 키를 지정함
cacheManager: Config에서 설정한 캐시 매니저를 지정함

스프링이 AOP를 이용하여 간편하게 캐싱을 적용할 수 있도록 도와줍니다.


Redis 캐시 적용 결과


조회하기 전에는 key가 비어있었지만, 조회 후에는 key가 Redis에 성공적으로 저장된 것을 확인할 수 있습니다.

첫 번째 조회

두 번째 조회

첫 번째 조회에는 Redis에 캐시가 저장되었고 두 번째 조회에는 캐시를 통해서 조회 성능이 1519ms에서 79ms19.22배 향상되었습니다.


마무리

Redis를 사용하면 메모리 비용이 증가하고 데이터 손실에 대한 위험이 있습니다.

하지만 캐싱으로 조회 성능 개선, 다양한 데이터 구조 지원, 메시지 브로커, 분산 락 등 다양한 장점을 가지고 있습니다. 따라서 Redis대용량 트래픽이 발생하는 환경과 분산 환경에서 활용하기 좋은 인메모리 데이터베이스 시스템입니다.

profile
📚개발 기록

0개의 댓글