처음 Redis에 대해 접한 것은 강대명님의 Redis 관련 강의 영상이었습니다.
홀린듯이 2시간에 달하는 영상을 모두 시청하고 강의를 보고 찾아본 Redis 관련 자료들은 더더욱 저를 매료시켰습니다. 다음에 느낀 감상은, 다음에는 기필코 Redis를 도입한 프로젝트를 진행해보자는 것이었습니다.
이런 강의를 들으면 항상 매혹되면서도 결국 직접 사용해보기 전에는 그것들에 대해서 제대로 감이 오지 않는 경우가 많았습니다. 결국 이해를 위해서는 그것을 도입해보는 과정이 필요하다고 생각했고 또한 캐싱은 성능 향상을 가장 극적으로 느낄 수 있는 기능 중 하나라고 생각하기도 했기에 프로젝트에 적용해보자고 마음 먹었습니다.
따라서 이번 글에서는 프로젝트에 Redis를 적용한 과정과 결과를 말씀드리고자 합니다.
프로젝트를 진행함에 앞서 Redis를 도입하는 것에 대한 팀원들에게 Redis를 적용하자고 설득할 수 있는 당위성이 필요하다고 생각했습니다. 프로젝트는 MicroK8s 기반으로 배포되고 있는 상황이었고, Scale Out이 발생할 수 있는 상황이었습니다. 따라서 ehcache와 같은 Local Cache를 사용하게되면 데이터 정합성 문제 등이 발생할 여지가 있었습니다.
따라서 Global Cache를 적용해야한다고 팀원들을 설득하였고, 본래라면 Redis와 Memcached라는 두 유명 오픈소스 사이에서 고민했어야 했겠지만, 저는 캐싱을 고민해보게 된 계기가 Redis 강좌였고, Memcache에 비해 Redis와 관련된 레퍼런스가 더 많았기에 참고할만한 자료가 더 많다고 판단했습니다.
또한,
https://aws.amazon.com/ko/elasticache/redis-vs-memcached/
AWS 공식 문서에서 서버의 동작 방식에 따라 두 In-memory DB 중 하나를 선택하라고 제시해준 자료입니다. 저는 이 자료에서 두 오픈소스 중 Redis의 장점을 더 강조하고 있다고 생각했습니다. 이러한 근거에 따라서 팀원들을 설득하였고, Redis를 도입하기로 결정하였습니다.
SpringBoot Redis 환경설정
SpringBoot 2.5.2 / Gradle 7.1.1 버전에서 작업한 예제임을 미리 말씀드립니다. 구체적인 작성 코드는 버전에 따라서 달라질 수 있습니다.
환경설정
Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.yml
spring:
cache:
host: (hostIP)
port: (redis port)
Gradle에 종속성을 설정하고, redis 서버가 있는 IP 주소와 포트번호를 application yml 파일에 세팅합니다.
CacheConfig.java
코드 설명
private final CacheProperties cacheProperties;
CacheProperties 클래스에 @ConfigurationProperties(prefix = "")로 application.yml 의 데이터를 끌어와 캐시의 지속시간 설정
- 참고) prefix를 "cache.teacherName"으로 바인딩 시에는 아래와 같은 변형도 모두 동일하게 인식합니다.
- cache.teacherName
- cache.teacher_name
- cache.teacher-name
- cache.TEACHER_NAME
-
@Getter @ConfigurationProperties(prefix = {application.yml에 세팅된 이름}) public class CacheProperties { private final Map<String, Long> ttl = new HashMap<>(); }
@Value("${spring.redis.cache.host}")
@value로 application.yml에 세팅한 값을 넣어주기
@Bean(name = "redisCacheConnectionFactory")
public RedisConnectionFactory redisCacheConnectionFactory() {
return new LettuceConnectionFactory(redisHost,
redisPort);
}
@Bean(name='...')을 지정한 이유는 제 프로젝트에서 Redis를 관심사에 따라서 분리하였고, 따라서 여러 개의 RedisConnectionFactory 가 존재했기 때문입니다. 만약 하나의 Redis만 올리시는 경우에는 별도의 네이밍을 지정하실 필요는 없습니다. Redis를 분리한 이유에 대해서는 다른 포스팅에서 다루겠습니다.
private ObjectMapper objectMapper() {
// jackson 2.10이상 3.0버전까지 적용 가능
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build();
return JsonMapper.builder()
.polymorphicTypeValidator(ptv)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.addModule(new JavaTimeModule())
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.build();
}
RedisCacheConfiguration의 valueSerializer인 Jackson2 는 LocalDate 타입을 인식하지 못합니다. 그에 따른 에러를 방지하기 위해 관련 모듈을 추가한 ObjectMapper를 Serializer에 전달하기 위한 메서드입니다.
private RedisCacheConfiguration redisCacheDefaultConfiguration() {
return RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(
new GenericJackson2JsonRedisSerializer(objectMapper())));
}
RedisCacheManager에 옵션을 부여할 수 있는 RedisCacheConfiguration 오브젝트입니다.
- defaultCacheConfig()
- key expiration : eternal
- cache null values : yes
- prefix cache keys : yes
- default prefix : [the actual cache name]
- key serializer StringRedisSerializer
- value serializer : JdkSerializationRedisSerializer
- default value serializer 옵션이 JdkSerialization 인데, 사람이 읽을 수 있는 구조로 저장하기 위해 json 형식으로 포맷팅 했습니다.
private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
for (Entry<String, Long> cacheNameAndTimeout : cacheProperties.getTtl().entrySet()) {
cacheConfigurations
.put(cacheNameAndTimeout.getKey(), redisCacheDefaultConfiguration().entryTtl(
Duration.ofSeconds(cacheNameAndTimeout.getValue())));
}
return cacheConfigurations;
}
cacheProperty에 세팅해둔 값들을 HashMap에 저장하여 관리하는 메서드입니다.
@Bean
public CacheManager redisCacheManager(
RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheDefaultConfiguration())
.withInitialCacheConfigurations(redisCacheConfigurationMap()).build();
}
Redis 서버의 분리로 인해 Config 파일이 여럿 존재하여 RedisConnectionFactory가 여럿 존재하는 경우, 두 가지 방법으로 RedisConnectionfacory를 주입할 수 있습니다.
메서드의 이름을 redisConnectionCacheFactory와 같이 변경하거나, Bean(name ="") 옵션으로 네이밍을 수정하고 Qualifier로 명시하여 주입하는 두 가지 방식 모두 동작합니다.
위에서 설정한 redis 설정들과 Cache 지속시간에 관한 속성을 추가하여 RedisCacheManager를 만들어내는 메서드입니다.
코드 전문은 아래와 같습니다.
@RequiredArgsConstructor
@EnableCaching
@Configuration
public class CacheConfig {
// 1번
private final CacheProperties cacheProperties;
// 2번
@Value("${spring.redis.cache.host}")
private String redisHost;
@Value("${spring.redis.cache.port}")
private int redisPort;
// 3번
@Bean
public RedisConnectionFactory redisCacheConnectionFactory() {
return new LettuceConnectionFactory(redisHost,
redisPort);
}
// 4번
private ObjectMapper objectMapper() {
// jackson 2.10이상 3.0버전까지 적용 가능
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build();
return JsonMapper.builder()
.polymorphicTypeValidator(ptv)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.addModule(new JavaTimeModule())
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.build();
}
// 5번
private RedisCacheConfiguration redisCacheDefaultConfiguration() {
return RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(
new GenericJackson2JsonRedisSerializer(objectMapper())));
}
// 6번
private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
for (Entry<String, Long> cacheNameAndTimeout : cacheProperties.getTtl().entrySet()) {
cacheConfigurations
.put(cacheNameAndTimeout.getKey(), redisCacheDefaultConfiguration().entryTtl(
Duration.ofSeconds(cacheNameAndTimeout.getValue())));
}
return cacheConfigurations;
}
// 7번
@Bean
public CacheManager redisCacheManager(
RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheDefaultConfiguration())
.withInitialCacheConfigurations(redisCacheConfigurationMap()).build();
}
}
이와 같이 세팅하게되면 SpringBoot 내부의 Redis 관련 환경설정은 모두 끝났습니다.
SpringBoot Redis Cache 적용
SpringBoot 2.5.2 / Gradle 7.1.1 버전에서 작업한 예제임을 미리 말씀드립니다. 구체적인 작성 코드는 버전에 따라서 달라질 수 있습니다.
SpringBoot 에서 Cache를 적용하는 방법은 비교적 간단합니다.
@Cacheable 로 캐싱할 메서드를 지정합니다.@CacheEvict로 캐시를 제거할 메서드를 지정합니다.@CachePut으로 무조건 메서드를 실행하고 그 결과를 캐시에 저장합니다.@Caching 은 CacheEvict나 CachePut등을 동시에 같은 메서드에 지정해야할 때 사용합니다.
@Cacheable과 CachePut의 차이점은, Cacheable은 cache hit이 발생했을 때 메서드를 실행하지 않고 캐싱 데이터를 곧바로 리턴하는 반면, CachePut은 무조건 메서드를 실행하고 캐시를 갱신한다는 점입니다. (당연하지만, 이 둘은 같은 메서드에 사용하는 것이 권장되지 않습니다.)
제가 진행하던 프로젝트에서 가장 조회가 잦은 것은 PT 트레이너의 목록을 조회하는 기능이었고, 또한 업데이트 역시 잦지 않을 것으로 판단했습니다. 따라서 목록 조회 메서드에 @Cacheable을, 트레이너 등록 메서드에 @CacheEvict를 적용하기로 결정했습니다.
@Cacheable(cacheNames = "teacherList", key = "#searchDto.hashCode() + #pageable.pageNumber")
@Transactional
public AllTeacherListResponse getTeacherList(SearchDto searchDto, Pageable pageable) {
//...
}
@Cacheable에는 여러가지 속성이 있지만, 제가 사용한 것은 캐시 이름을 설정하는 cacheNames, key 였습니다. 추가적으로 자주 사용할만한 속성들로 몇가지 더하여 정리해보고자 합니다.
- cacheNames(=value)
- 캐시 이름을 설정하는 속성입니다.
- key
- 캐시의 key를 정하는 속성입니다.
- 제 프로젝트에서 PT 트레이너 목록 조회 메서드의 경우에는 SearchDto로 조건에 맞는 트레이너만 조회할 수 있도록 되어있는 메서드였으므로, 검색 조건에 따라 키값을 설정할 필요가 있었습니다. 또한 클라이언트 측의 페이징 데이터에 따라 20개씩 데이터를 잘라서 넘겨주고 있던 상황이었으므로, PageNumber에 대한 정보까지 포함하여 키값으로 세팅하였습니다.
- condition
- 상황에 따라 캐시를 할지 말지 결정하는 속성입니다.
- condition = "#pageable.pageNumber >0" 와 같은 형식으로 설정할 수 있습니다.
@CacheEvict(cacheNames = "teacherList", allEntries = true)
@Transactional
public void signup(SaveUserRequest userRequest) {
//...
}
PT 트레이너가 새로 가입하게 되면 teacherList 조건에 따른 모든 캐싱이 초기화되도록 설정한 메서드입니다. CacheEvict에서 자주 사용할만한 속성은 다음과 같습니다.
- key
- 지정된 key 값의 캐시를 삭제하는 속성입니다.
- cacheNames(value)
- 지정된 cacheNames의 캐시를 삭제하는 속성입니다.
- allEntries
- 전체 캐시를 지울지에 대한 여부를 지정하는 속성입니다.
이와 같은 과정으로 캐시를 적용하였습니다.
'프로젝트 > O-GYM' 카테고리의 다른 글
SpringBoot 프로젝트에 JWT 토큰 인증 방식 구현하기 (11) | 2021.10.08 |
---|