초고속 성능, 다양한 데이터 구조, pub/sub라는 키워드와 연관된 기술인 Redis에 대해 알아보았습니다. 캐싱이나 실시간 분석과 관련된 문제를 해결할 때 해결책으로 자주 언급되는 Redis는 구체적으로 어떤 역할을 하고 왜 사용해야 하는지 정리해보겠습니다.
Redis는 비관계형 데이터베이스(NoSQL)로 분류되며 다양한 형태로 데이터를 저장합니다. 여러 데이터 모델 중에서도 Redis는 특히 키-값을 쌍으로 저장하는 방식을 사용할 때 유용합니다. 이외에도 문자열, 리스트, 세트, 해시, 정렬된 세트 등의 데이터 구조도 지원합니다.
인메모리 데이터 저장소란 데이터를 RAM이라는 메모리에 저장하는 것을 뜻합니다. 메모리에 데이터를 저장하면 디스크에 저장할 때보다 빠른 액세스가 가능하기 때문에, 데이터 읽고 쓸 때 발생하는 시간을 단축할 수 있습니다. 필요에 따라서는 디스크에 데이터를 저장하는 것도 가능합니다. 특정 주기마다 디스크에 백업하도록 설정해서 데이터의 영속성을 보장하도록 설정할 수 있습니다.
빠른 접근 속도를 가능하게 하는 캐싱 기능을 구현하기 위해 사용합니다.
만약 특정 데이터가 반복적으로 요청된다면 캐싱 기능을 사용해 데이터를 저장하는 것이 효율적일 수 있습니다. Redis를 활용해 데이터를 메모리에 저장하면 빠른 응답 속도를 기대할 수 있습니다. 자주 갱신되며 일시적으로 저장한다는 특징을 가진 데이터를 주로 캐싱합니다.
위에서 언급한 캐싱의 목적에 정확히 일치하지는 않지만, 캐싱을 많이 사용하는 예시 중 하나가 로그인 세션 관리입니다. 사용자 로그인 세션 데이터는 잦은 갱신을 요구한다고 보기는 어렵지만, 빠른 액세스가 필요하다는 점에서는 캐싱 목적으로 Redis를 활용한다고 볼 수 있습니다. 사용자가 로그인한 상태에서 보내는 모든 요청이 매번 JWT 인증이나 권한을 확인하기 때문에 데이터를 빠르게 읽는 것이 중요합니다.
다만, 로그인 세션 데이터는 일반적으로 로그인시 생성되어 로그아웃이나 세션 만료시까지 유지되어야 하기 때문에 갱신을 목적으로 하는 캐싱으로 보긴 어렵겠습니다. 아래 내용에서 Redis의 세션 관리 기능에 대해 더 알아보겠습니다.
안정적인 상태 정보 관리가 필요한 세션 관리를 위해 사용합니다.
🔎 세션의 분류
- 사용자 세션: 인증 정보, 사용자 설정, 장바구니 데이터, 임시 데이터 (폼 입력, 진행 상태) 등
- 어플리케이션 세션: 어플리케이션 관련 상태, 분산 트랜잭션 상태, 처리 중인 작업 상태 등
사용자 세션 관리 가 필요한 어플리케이션에서 Redis의 이점을 특히 잘 활용할 수 있습니다. 사용자가 어플리케이션에 로그인한 후에 일어나는 여러 상태 정보(인증 정보, 어플리케이션 개인 설정, 장바구니 정보 등)를 안정적으로 관리할 수 있는 기능을 Redis가 제공하기 때문입니다.
복사한 동일한 데이터를 사용해 지속적으로 서비스를 제공할 수 있습니다. Redis 클러스터를 사용해서 단일 노드를 master-slave 노드로 확장하여 구축하고, Redis Sentinel을 사용해서 master 인스턴스에 장애가 있을 경우 slave 인스턴스를 마스터로 승격 시켜 서비스를 지속합니다.생성된 데이터를 즉각적으로 처리해야 하는 경우에 사용합니다.
🔎 메시징 시스템 종류
Redis: 빠른 읽기/쓰기와 단순한 데이터 구조
Apache Kafka: 높은 처리량을 제공해서 대규모 메시지 처리 가능
RabbitMQ: 라우팅 로직을 다르게 해서 다양한 메시지 패턴 지원
동일한 자원에 대한 동시 접근을 방지하는 분산락을 활용하기 위해 사용합니다.
Jedis, Redisson, Lettuce 같은 클라이언트 라이브러리가 제공하는 특정 명령어를 사용하면 분산락을 구현할 수 있습니다. 해당 명령어들이 트랜잭션처럼 작동해서 수행 중간에 다른 명령어가 끼어들지 않도록 보장합니다.
SpringBoot, Redis, Redisson을 활용해서 캐싱 기능을 사용하고자 하는 경우 아래처럼 코드를 작성할 수 있습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.redisson:redisson-spring-boot-starter:3.16.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring.redis.host=localhost
spring.redis.port=6379
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
config.setCodec(new JsonJacksonCodec()); // JSON 직렬화 설정
return Redisson.create(config);
}
}
RedisCahceManager를 생성해 캐시 키-값을 직렬화하는 방식을 설정
🔎 직렬화 설정
객체 데이터를 바이트 스트림으로 변환하는 것을 직렬화라고 한다. Redis 캐시 시스템에 객체를 저장하려면 직렬화가 필요합니다. 키는 문자열로, 값은 JSON 형식으로 직렬화하는 것이 일반적입니다.
LettuceConnectionFactory를 생성해 Redis 서버와의 연결을 설정
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Configuration
public class CacheConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean // Redis를 캐시로 사용하기 위한 설정을 관리
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(conf)
.build();
}
@Bean // Redis 서버와의 연결을 설정
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();
conf.setHostName(this.host);
conf.setPort(this.port);
return new LettuceConnectionFactory(conf);
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableCaching
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@Cacheable(key = "#companyName", value = CacheKey.KEY_FINANCE)
// companyName으로 정보를 조회하는 메소드 예시
public ScrapedResult getDividendByCompanyName(String companyName) {
CompanyEntity company = this.companyRepository.findByName(companyName)
.orElseThrow(() -> {
log.warn("no company found with name {}", companyName);
return new NoCompanyException();
});
💡 캐싱 사용시 주의점
캐시 미스를 최소화하기 위해서는 (1) 자주 호출되는 메소드에 캐시를 적용하고 (2) 메소드 별로 교유한 캐시 키를 사용해 데이터 충돌을 막고 (3) 적절한 만료 시간을 설정하는 것이 중요합니다.