Cache 기능 Redis로 구현하기
https://pamyferret.tistory.com/8
https://pamyferret.tistory.com/9
저번에 SpringBoot의 캐시 기본기능과 Redis를 Window에 설치하는 방법에 대해 글을 올렸었다.
SpringBoot 캐시 기본 기능에서는 캐시 데이터를 저장하는 저장소를 애플리케이션이 구동됐을 때 메모리를 차지하는 방식으로 사용했었는데, Redis로 설치했겠다 Redis를 캐시 데이터 저장소로 이용해보도록 하겠다.
SpringBoot Redis 연결
먼저 Redis에 캐시 데이터를 저장하기 위해서 SpringBoot에 Redis를 연결한다.
SpringBoot에 Redis를 연결하는 것을 그렇게 어렵지 않다.
1. Redis 사용을 위한 라이브러리 설정
DB를 연결해 사용하기 위해서는 해당 DB에 필요한 라이브러리를 설정해서 사용하듯이 Redis 또한 Redis 사용을 위한 라이브러리 설정을 해야 한다.
- gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
- maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. Redis 접속 정보 설정
라이브러리 설정을 정상적으로 마쳤다면 application.properties(application.yml)에 아래와 같이 redis 접속 정보를 입력한다.
(application.yml에서는 작성 형태가 트리 형태로 달라진다.)
만일 redis에 별도 유저와 비밀번호가 설정되어 있지 않다면 spring.redie.username과 spring.redis.password 부분은 생략이 가능하다.
spring.cache.type=redis
spring.redis.host=${redis_host}
spring.redis.port=${redis_port}
spring.redis.password=${password}
spring.redis.username=${user_name}
Redis 사용을 위한 Config Class 파일 작성
여기까지 하면 redis를 연결하는 정보들은 이미 application.properties(application.yml)에 작성해서 redis에 연결은 됐지만 캐싱 기능을 사용하는데 redis를 사용하겠다고 설정이 되지 않은 상태이다. (redis 자체는 이미 SpringBoot에 연결된 상태이다.)
캐싱 기능에 redis를 사용하겠다고 설정하는 것은 별도 Config 파일을 작성해서 설정해야한다.
Config 파일을 작성하는 폴더에 CacheManager 또는 CacheConfig와 같이 알아볼 수 있는 이름의 class 파일을 생성하고 아래 내용을 입력한다.
@Configuration
public class CacheManager {
@Value("${spring.redis.port}")
public int port;
@Value("${spring.redis.host}")
public String host;
@Value("${spring.redis.username}")
public String userName;
@Value("${spring.redis.password}")
public String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setUsername(userName);
redisStandaloneConfiguration.setPassword(password);
LettuceConnectionFactory lettuceConnectionFactory =
new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFacroty())
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
우선 Cache 설정 파일을 작성하는 것이므로 @Configuration이라는 어노테이션을 붙여 해당 클래스가 설정을 위한 클래스라는 것을 명시해준다.
또한 위에서 application.properties(application.yml)에 작성해놨던 redis 접속 정보를 변수로 가져온다.
@Configuration
public class CacheManager {
@Value("${spring.redis.port}")
public int port;
@Value("${spring.redis.host}")
public String host;
...
}
그리고 안에 Bean을 두 개 만들어준다. 하나는 redis 연결 정보를 가지고 캐싱 기능을 연결시킬 Bean이고 다른 하나는 캐시에서 Redis를 사용하기 위한 Bean이다.
@Configuration
public class CacheManager {
...
@Bean
public RedisConnectionFactory redisConnectionFactroy() {
...
}
@Bean
public RedisCacheManager redisCacheManager() {
...
}
}
RedisConnectionFactory
우선 첫 번째 Bean인 redisConnectionFactory()는 RedisConnectionFactory라는 객에 redis 접속 정보를 입력해 캐싱 기능을 명시한 곳에 사용할 수 있도록 설정 정보를 담은 객체이다.
이 Bean을 캐싱 설정을 하는 redisCacheManager()에서 받아서 사용할 것이다.
@Bean과 @Component의 간단한 차이 점은 아래 글 참고 바란다.
https://pamyferret.tistory.com/23
코드를 보면 복잡해보일 수 있지만 차근차근 보면 그렇게 어렵지는 않다.
RedisStandaloneConfiguration 객체를 통해 redis 접속 정보를 갖고 있는 객체를 port, host, password 등 정보를 넣어서 설정한다.
여기서 RedisStandaloneConfiguration은 single node에 redis를 연결하기 위한 설정 정보를 가지고 있는 기본 클래스이다.
여기서도 마찬가지로 userName과 password는 생략할 수 있다.
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setUsername(userName);
redisStandaloneConfiguration.setPassword(password);
LettuceConnectionFactory lettuceConnectionFactory =
new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
}
만들어진 redis 설정 정보가 담긴 RedisStandaloneConfiguration을 LettuceConnectionFactory에 담아서 반환하는데,
RedisConnectionFactory는 인터페이스 해당 인터페이스를 상속 받아 구현한 LettuceConnectionFactory로 최종적으로 만들어서 반환하는 것이다.
(마치 List 인터페이스를 해당 인터페이스를 상속 받아 구현한 ArrayList로 생성하는 것과 같이)
LettuceConnectionFactory lettuceConnectionFactory =
new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
RedisCacheManager
redis 연결을 위한 정보를 설정한 객체 RedisConnectionFactory Bean이 구현이 끝났다면,
이제 본격적으로 캐싱 기능에 redis를 사용하도록 설정하는 차례가 남았다.
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFacroty())
.cacheDefaults(redisCacheConfiguration)
.build();
}
이 부분도 이것저것 설정이 많이 들어가는 것 같이 보이지만 한 줄 한 줄 보면 그렇게 복잡한 설정은 아니다.
우선 redis 캐싱 기능을 위해 RedisCacheConfiguration으로 설정 객체를 생성한다. 객체는 defaultCacheConfig()로 생성을 하되 그 안에서 key 값과 value 값을 어떻게 직렬화 시킬 것인지 정의를 해준다. 이렇게 어떻게 직렬화 시킬 것인지 정의를 하지 않으면 redis 캐싱 기능을 사용하는 중 직렬화 정의가 안 되어 있어 redis에 캐시 데이터를 저장할 수 없는 에러가 발생한다.
우선 serializeKeyWith()을 통해서 redis에 저장할 key 값을 직렬화 시키는 방법에 대해 정의한다.
이 때 RedisSerializationContext를 이용하는데 이 객체에는 key, value 등에 대해 직렬화 할 수 있는 방법을 제공하는 인터페이스이다.
RedisSerializationContext.SerializationPair.fromSerializer()은 직렬화에 사용할 어댑터를 함수 안에 넣은 타입에 맞게 반환하는 static 함수이다.
따라서 serializeKeyWith()만 떼놓고 보면 RedisSerializationContext.SerializationPair.fromSerializer() 함수에 StringRedisSerializer 클래스를 넣어 key 값을 직렬화 redis에 저장할 때 어떻게 직렬화 시킬 것인지 정의를 한다.
여기서 StringRedisSerializer는 String을 UTF-8 기준으로 byte[]로 변환시키거나 또는 그 byte[]를 UTF-8 String으로 변환시키는 것이다.
즉 아래와 같은 설정은 기본 key String 값을 byte[](UTF-8)로 변환해서 저장하고 변환해서 가져오는 것을 설정한 것이다.
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
Values를 직렬화 하는 방법을 정의한 serializeValuesWith()을 보면 다른건 다 serializeKeysWith()랑 동일한데 fromSerializer안의 내용만 다르다. 왜냐하면 Values의 경우 단순하게 String이 아니라 Object 형태이므로 Json 형태로 직렬화 해 redis에 저장하고 꺼내오고 할 필요가 있기 때문이다.
GenericJackson2JsonRedisSerializer 클래스는 redis에서 Object를 JSON화 시켜주기도 하고 JSON을 Object화 시켜주기도 한다.
즉, 캐시 데이터(Object)를 JSON으로 직렬화 해 redis에 저장하고 다시 redis에서 가져올 때는 JSON을 Object화 해서 가져온다는 의미이다.
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
위에서 key와 value를 어떻게 직렬화해 redis에 저장할지에 대한 정의를 다 끝냈다면 RedisCacheManager에서 RedisCacheManagerBuilder를 통해 RedisCacheManager를 정의해 build한 후 반환하면 된다. 이와 같이 builder를 이용하는 것을 Build 패턴이라고 해서 DTO나 Repository에서도 사용하는데 그건 추후 다시 설명하도록 하겠다.
참고로 여기서 위에 정의해놨던 RedisConnectionFactory Bean 객체를 넣어준다.
@Bean
public RedisCacheManager redisCacheManager() {
...
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFacroty())
.cacheDefaults(redisCacheConfiguration)
.build();
}
이와 같이 설정하면 redis에 캐시 데이터를 저장해 캐싱 기능을 사용할 수 있다. @Configuration 어노테이션으로 인해 자동으로 스프링부트 컨테이너에서 인식해 캐싱 설정을 하게되고 캐시 데이터는 redis에 저장되게 된다.
(분산 캐싱을 구현하기 위해 여러 CacheManager를 정의한 경우 별도의 설정이 필요하다. 이것 또한 추후에 다시 글로 작성하겠다.)
실행 예
전에는 캐시 데이터가 애플리케이션 메모리에 저장되어서 제대로 저장되었는지 key 값을 검색해서 확인하는 작업 등을 하지 못했다.
하지만 이제는 별도의 캐시 데이터 저장 서버인 redis를 사용하므로 캐시 데이터가 저장되면 redis에서 해당 캐시 데이터가 저장된 것을 확인할 수 있다.
참고로 redis 서버에 캐시 데이터가 저장되는 것이므로 캐시 데이터를 저장하는 애플리케이션이 돌아가는 동안에는 redis 서버가 켜진 상태여야 한다. 안 그럼 connection 에러가 발생한다.
https://pamyferret.tistory.com/8
위 글에서 캐시 데이터를 저장하는 getMenuList() 함수를 사용할 경우 아래와 같이 DB에서 처음에는 데이터를 읽어고 그 다음에는 캐시 데이터를 읽어온다.
이 때 연결되어 있는 redis의 client 화면(redis-cli.exe)를 실행시켜서 'keys *' 명령어로 key 값을 모두 검색해보면 내가 캐싱 설정해놓은대로 menu 이름을 가진 캐시에 Simple Key[](key value를 지정하지 않았으므로) key 값으로 잘 들어가져 있는 것을 확인할 수 있다.