Spring Cache 알아보기 - 2 (redis편)
2024년 08월 24일 17:00:00
목차
- No items found
개발을 하며 알게 된 것들을 개인적으로 적고 공유 하는 거라 내용이 많이 틀릴 수 있습니다.
이번에는 스프링부트에서 redis를 이용하여 CacheManager를 적용하며 여러가지 정보들을 정리하려고 합니다.
CacheManager
1편에서 간단히 이야기하였던 추상화된 캐시를 사용하기 위해 스프링에서 제공하는것이 Cache Manager
입니다.
다양한 캐시매니저가 있는데 여기서는 redis를 이용하도록 하겠습니다.
스프링 Data Redis 의존성 추가
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis/3.3.3 Maven 저장소로 갑시다. (작성시점 스프링부트 버전임)
implementation("org.springframework.boot:spring-boot-starter-data-redis:3.3.3")
프로젝트의 build.gradle에 추가해줍시다.
CacheManager
추상화된 캐시를 사용하기 위해 스프링에서 제공하는것이 Cache Manager
라고 했던것을 1편에서 이야기 했었죠.
인텔리지에서 구현체 검색했을때의 목록 1편에서는 RedisCacheManager
가 없었는데 추가 된것을 확인할 수 있습니다.

프로젝트 캐시매니저 설정에 redisCacheManager 추가하기
1편에서 로컬 캐시매니저로 CaffeineCacheManager를 추가했던 내용이 있을텐데 여기에 redisCacheManager를 추가해봅시다.
스프링에서는 여러 캐시 매니저도 어노테이션으로 쉽게 사용할 수 있게 구현되어있기 때문에 교체가 아닌 추가로 사용해봅시다.
저번과 다르게 뭔가 많은 코드들이 추가 된것 같아보이는데 설명과 함께 같이 보시죠.
@EnableCaching
@Configuration
class CacheConfig(
private val redisProperties: RedisProperties,
) {
companion object {
const val DEFAULT_TTL = 5L
const val CAFFEINE_CACHE_MANAGER = "caffeineCacheManager"
const val REDIS_CACHE_MANAGER = "redisCacheManager"
const val FINANCIAL_PRODUCTS = "financialProducts"
const val FINANCIAL_PRODUCT = "financialProduct"
}
@Bean(name = [CAFFEINE_CACHE_MANAGER])
fun caffeineCacheManager(): CacheManager =
CaffeineCacheManager().apply {
setCaffeine(
Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.expireAfterWrite(JwtProvider.REFRESH_TOKEN_VALID_MILLISECONDS, TimeUnit.MILLISECONDS)
.weakKeys()
.recordStats(),
)
}
@Primary
@Bean(name = [REDIS_CACHE_MANAGER])
fun redisCacheManager(
redisConnectionFactory: RedisConnectionFactory,
redisSerializer: RedisSerializer<Any>,
): CacheManager =
RedisCacheManagerBuilder.fromCacheWriter(
RedisCacheWriter.nonLockingRedisCacheWriter(
redisConnectionFactory,
UnlinkScanBatchStrategy(batchSize = redisProperties.batchSize),
),
).cacheDefaults(this.createRedisCacheConfiguration(redisSerializer = redisSerializer))
.withInitialCacheConfigurations(
mapOf(FINANCIAL_PRODUCTS to this.createRedisCacheConfiguration(redisSerializer = redisSerializer, redisTtl = 300L)),
)
.transactionAware()
.build()
@Bean
fun redisSerializer(): RedisSerializer<Any> =
GenericJackson2JsonRedisSerializer(
ObjectMapper()
.registerModules(
JavaTimeModule(),
KotlinModule.Builder().build(),
)
.setSerializationInclusion(JsonInclude.Include.ALWAYS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder().allowIfSubType(Any::class.java).build(),
ObjectMapper.DefaultTyping.EVERYTHING,
JsonTypeInfo.As.PROPERTY,
).enable(SerializationFeature.INDENT_OUTPUT),
)
@Bean
fun redisConnectionFactory(): LettuceConnectionFactory =
LettuceConnectionFactory(
RedisStandaloneConfiguration(redisProperties.host, redisProperties.port).apply {
database = redisProperties.database
},
LettuceClientConfiguration.builder().commandTimeout(Duration.ofMillis(redisProperties.commandTimeout)).build(),
)
@Bean
fun redisTemplate(
connectionFactory: LettuceConnectionFactory,
redisSerializer: RedisSerializer<Any>,
): RedisTemplate<String, Any> {
val keySerializer = StringRedisSerializer()
return RedisTemplate<String, Any>().apply {
setConnectionFactory(connectionFactory)
setKeySerializer(keySerializer)
setValueSerializer(redisSerializer)
}
}
private fun createRedisCacheConfiguration(
redisSerializer: RedisSerializer<Any>,
redisTtl: Long = DEFAULT_TTL,
): RedisCacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(redisTtl))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
}
redisCacheManagerBuilder를 이용하여 구현체 등록하기
RedisCacheManager.class
내부를 보시면 RedisCacheManager 구현체를 생성하는데에는 여러 함수들을 제공해주고 있습니다.
그중에서 저희는 RedisCacheWriter
를 커스텀하게 작성하기 위해 fromCacheWriter
함수를 이용해서 등록하려합니다. (따로 커스텀이 필요하지 않다면 fromConnectionFactory
를 사용하면됩니다.)
CacheWriter
를 커스텀하게 등록하려는 이유는 cache객체에서 사용되는 clear를 커스터마이징하려합니다.
캐시 인터페이스를 보면 사용자에게 다양한 기능들을 제공해주고 있습니다. 각각의 메서드 이름에서 느껴지듯이 조회, 등록, 삭제등을 지원해주고 있습니다.
public interface Cache {
String getName();
Object getNativeCache();
@Nullable
ValueWrapper get(Object key);
@Nullable
<T> T get(Object key, @Nullable Class<T> type);
@Nullable
<T> T get(Object key, Callable<T> valueLoader);
@Nullable
default CompletableFuture<?> retrieve(Object key) {
throw new UnsupportedOperationException(this.getClass().getName() + " does not support CompletableFuture-based retrieval");
}
default <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
throw new UnsupportedOperationException(this.getClass().getName() + " does not support CompletableFuture-based retrieval");
}
void put(Object key, @Nullable Object value);
@Nullable
default ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
ValueWrapper existingValue = this.get(key);
if (existingValue == null) {
this.put(key, value);
}
return existingValue;
}
void evict(Object key);
default boolean evictIfPresent(Object key) {
this.evict(key);
return false;
}
void clear();
// 이하 생략
}
그런데 여기서 아무 생각없이 사용해도 되지만 redis구현체는 어떤 방법으로 clear하는지 궁금하기도하고 성능상의 이슈는 없을까? 하는 고민이 있어 내부를 확인해보았습니다.
public void clear() {
this.clear("*");
}
public void clear(String keyPattern) {
this.getCacheWriter().clean(this.getName(), this.createAndConvertCacheKey(keyPattern));
}
보면 CacheWriter의 clean을 또 호출하는것을 볼 수 있습니다. 그럼 코드를 또 확인해봅시다.
public void clean(String name, byte[] pattern) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(pattern, "Pattern must not be null");
this.execute(name, (connection) -> {
boolean wasLocked = false;
try {
if (this.isLockingCacheWriter()) {
this.doLock(name, name, pattern, connection);
wasLocked = true;
}
long deleteCount;
for(deleteCount = this.batchStrategy.cleanCache(connection, name, pattern); deleteCount > 2147483647L; deleteCount -= 2147483647L) {
this.statistics.incDeletesBy(name, Integer.MAX_VALUE);
}
this.statistics.incDeletesBy(name, (int)deleteCount);
return "OK";
} finally {
if (wasLocked && this.isLockingCacheWriter()) {
this.doUnlock(name, connection);
}
}
});
}
코드를 확인해보면 this.isLockingCacheWriter()에서 lock, unlock인지 확인하고 locking이면 lock을 걸고 있네요.
일단 저는 락까지 걸면서 지워야할 데이터는 아니기에 nonLockingRedisCacheWriter
을 이용하여 생성하였습니다.
public static RedisCacheManagerBuilder fromConnectionFactory(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
return new RedisCacheManagerBuilder(cacheWriter);
}
이걸 따로 신경쓰지 않고 fromConnectionFactory를 이용하여 생성하면 기본적으론 nonLockingRedisCacheWriter
가 사용되는것을 알 수 있습니다.
아니 그럼 여기서 든 의문이 뭐 이미 락도 안걸고 잘 지우고 있는걸 사용하고 있는것 같구만 굳이 커스터마이징 해야해? 라고 생각 할 수 있죠
(예, 저도 처음엔 그렇게 생각했는데 혹시나 하는마음에 redis와 내부 구현체를 찾아보게 되었습니다.) 다시 DefaultRedisCacheWriter
구현체 내부를 보면 실질적으로 캐시를 삭제하는 부분은
this.batchStrategy.cleanCache(connection, name, pattern);
이부분 같아 보이네요 이름에서 느껴지는게 반복문을 돌면서 특정 사이즈만큼씩 삭제를 하는것 같습니다.
그렇게 cleanCache의 구현체를 확인하기 위해 눌러보면 아래같은 추상 클래스가 등장하게 됩니다.
public abstract class BatchStrategies {
public static BatchStrategy keys() {
return BatchStrategies.Keys.INSTANCE;
}
public static BatchStrategy scan(int batchSize) {
Assert.isTrue(batchSize > 0, "Batch size must be greater than zero");
return new Scan(batchSize);
}
private BatchStrategies() {
}
static class Keys implements BatchStrategy {
static Keys INSTANCE = new Keys();
Keys() {
}
public long cleanCache(RedisConnection connection, String name, byte[] pattern) {
byte[][] keys = (byte[][])((Set)Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())).toArray(new byte[0][]);
if (keys.length > 0) {
connection.del(keys);
}
return (long)keys.length;
}
}
static class Scan implements BatchStrategy {
private final int batchSize;
Scan(int batchSize) {
this.batchSize = batchSize;
}
public long cleanCache(RedisConnection connection, String name, byte[] pattern) {
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count((long)this.batchSize).match(pattern).build());
long count = 0L;
PartitionIterator<byte[]> partitions = new PartitionIterator(cursor, this.batchSize);
while(partitions.hasNext()) {
List<byte[]> keys = partitions.next();
count += (long)keys.size();
if (keys.size() > 0) {
connection.del((byte[][])keys.toArray(new byte[0][]));
}
}
return count;
}
}
static class PartitionIterator<T> implements Iterator<List<T>> {
private final Iterator<T> iterator;
private final int size;
PartitionIterator(Iterator<T> iterator, int size) {
this.iterator = iterator;
this.size = size;
}
public boolean hasNext() {
return this.iterator.hasNext();
}
public List<T> next() {
if (!this.hasNext()) {
throw new NoSuchElementException();
} else {
List<T> list = new ArrayList(this.size);
while(list.size() < this.size && this.iterator.hasNext()) {
list.add(this.iterator.next());
}
return list;
}
}
}
}
일단 함수내에 2가지 구현체가 있네요 Scan과 Keys 간단히 설명하자면 아래와 같아요.
- KEYS 명령어
KEYS 명령어는 패턴과 일치하는 모든 키를 한 번에 반환합니다. 예를 들어, KEYS user:*는 "user:"로 시작하는 모든 키를 반환합니다.
장점특정 패턴과 일치하는 모든 키를 빠르게 얻을 수 있습니다.
단점성능 문제: Redis는 단일 스레드로 작동하기 때문에, KEYS 명령어가 실행되면 서버는 모든 키를 검색하고 결과를 반환할 때까지 다른 작업을 처리할 수 없습니다.
즉, 데이터베이스에 키가 많을 경우(수백만 개 이상), 이 명령어는 Redis 서버를 일시적으로 멈출 수 있습니다.
블로킹 문제: KEYS 명령어는 매우 비효율적이며, 운영 환경에서 사용하면 큰 부하를 유발할 수 있습니다. 특히 프로덕션 환경에서는 사용하지 않는 것이 좋습니다.
- SCAN 명령어
SCAN 명령어는 KEYS와 유사하게 패턴과 일치하는 키를 검색하지만, 모든 결과를 한 번에 반환하는 대신 반복적으로 호출하여 부분적으로 키를 반환합니다.
장점비차단형: SCAN은 블로킹 작업이 아니므로 Redis 서버가 다른 작업을 수행하는 데 큰 영향을 주지 않습니다. 따라서 프로덕션 환경에서 안전하게 사용할 수 있습니다.
점진적 검색: SCAN은 결과를 작은 청크(chunk) 단위로 반환하기 때문에 서버 자원 소모가 KEYS보다 훨씬 적습니다. 이는 대량의 키를 처리할 때 특히 유용합니다.
단점중복 키: SCAN을 사용하면 동일한 키가 여러 번 반환될 수 있으므로, 이러한 중복을 처리해야 합니다.
결과 완전성 보장 없음: SCAN은 호출할 때마다 다른 결과를 반환할 수 있으며, 모든 키가 반환될 때까지 반복적으로 호출해야 합니다.
요약
KEYS: 빠르지만 성능에 큰 영향을 미치며, 많은 키가 있는 환경에서는 주의해서 사용해야 합니다.
SCAN: 성능에 영향을 적게 미치고, 대량의 키를 검색할 때 적합하지만 중복 처리와 여러 번 호출이 필요합니다.
대충 생각해보면 운영환경에선 KEYS를 사용해서 대량으로 데이터를 지우게되면 블로킹이 되어 장애까지 일어날 수 있겠다는 생각이 풀풀 들지 않나요?
그래서 잘은 모르지만 클라우드 환경에서 제공하는 redis의 경우 KEYS명령어 자체를 막아놓은 경우도 있다고합니다.
어쨋든 그러면 nonLockingRedisCacheWriter은 어떤걸 사용하고 있을까?
static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
return nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.keys());
}
static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
return lockingRedisCacheWriter(connectionFactory, BatchStrategies.keys());
}
짜잔 기본으로 적용되는 친구들은 무려 Keys를 사용하고 있습니다. (어디선가 듣기론 하위호환성을 위해서 Keys로 되어있다고하더군요.)
만약 엄청나게 많은 트래픽 및 redis를 빈번하게 clear를 호출하게 된다면 어떻게 될까요?? 큰일 날 것 같은 기분이 들어 RedisCacheWriter를 직접 등록하도록 작성하게 되었습니다.
여기서 또 생각된게 어쨋든 Scan을 이용하여 chunk단위로 삭제한다고해도 그 청크단위만큼 삭제시에 블로킹이 걸리는건 마찬가지인 것 같은데 더 좋은방법이 없나 찾게 되었습니다.
(물론 이정도만 적용하여도 대부분 문제는 발생하지 않을 것 같긴합니다.)
찾아보니 Unlink라는 명령어를 찾게 되었습니다. 다시 또 GPT 씨의 말을 빌리자면 아래와 같은 차이점이 있습니다.
Redis UNLINK 명령어
UNLINK 명령어는 Redis에서 데이터를 삭제하는 방법 중 하나로, 특히 큰 데이터 구조나 많은 키를 삭제할 때 성능에 미치는 영향을 줄이기 위해 도입되었습니다.
이 명령어는 DEL 명령어와 유사하지만, 동작 방식에서 중요한 차이점이 있습니다.
-
DEL 명령어 설명: DEL 명령어는 지정된 키를 삭제합니다. 삭제 과정에서 Redis는 해당 키와 연관된 메모리를 즉시 해제합니다. 단점 블로킹 작업: DEL 명령어는 키와 연관된 모든 메모리를 해제할 때까지 블로킹됩니다. 작은 키들은 문제가 되지 않지만, 큰 데이터 구조(예: 큰 리스트, 세트, 해시 등) 또는 다수의 키를 삭제할 때는 삭제 작업이 완료될 때까지 Redis 서버의 응답이 느려질 수 있습니다. 이는 성능 저하를 일으키고, 특히 실시간으로 많은 요청을 처리하는 서버에서는 문제가 될 수 있습니다.
-
UNLINK 명령어 설명: UNLINK 명령어는 DEL 명령어와 유사하게 키를 삭제하지만, 큰 차이점은 메모리 해제를 비동기적으로 처리한다는 점입니다. 즉, UNLINK 명령어를 호출하면 해당 키가 즉시 삭제되지만, 실제로 메모리가 해제되는 작업은 백그라운드에서 비동기적으로 이루어집니다. 장점 비차단형 삭제: UNLINK 명령어는 키를 삭제하는 즉시 제어를 반환하므로, Redis 서버는 다른 작업을 지연 없이 처리할 수 있습니다. 이는 큰 데이터 구조를 삭제할 때도 서버 성능에 미치는 영향을 최소화합니다. 효율적인 메모리 관리: 비동기적인 메모리 해제 덕분에 대규모 삭제 작업이 서버 성능에 미치는 영향이 적습니다.
요약
DEL: 동기적으로 키를 삭제하며, 큰 데이터 구조를 삭제할 때는 성능 저하를 유발할 수 있습니다.
UNLINK: 비동기적으로 키를 삭제하여 서버 성능에 미치는 영향을 최소화합니다.
어 그러면 스프링에서도 구현되어 있는게 있을까? 하여 찾아봤으나 구현체를 따로 찾지 못하여 직접 구현체를 만들게 되었습니다.
class UnlinkScanBatchStrategy(
private val batchSize: Int,
) : BatchStrategy {
override fun cleanCache(
connection: RedisConnection,
name: String,
pattern: ByteArray,
): Long =
connection.keyCommands().scan(ScanOptions.scanOptions().count(this.batchSize.toLong()).match(pattern).build()).asSequence()
.chunked(this.batchSize)
.map { keys ->
connection.keyCommands().unlink(*keys.toTypedArray())
keys.size.toLong()
}.sum()
}
대부분 Scan 구현체 내부의 코드를 참고하여 커맨드만 unlink형태로 수정하였습니다.
그럼 이제 다시 redis CacheManager
를 등록하는 부분으로 돌아와 봅시다.
RedisCacheManagerBuilder의 다양한 옵션들
-
allowCreateOnMissingCache 런타임에 캐시를 자동으로 생성할지 여부를 설정합니다. 기능: true면 캐시를 요청할 때 자동 생성합니다.
-
disableCreateOnMissingCache 런타임 동안 캐시 자동 생성을 비활성화합니다. 기능: 자동 생성을 원하지 않을 때 사용합니다.
-
enableCreateOnMissingCache 런타임 동안 캐시 자동 생성을 활성화합니다. 기능: 존재하지 않는 캐시가 요청될 때 자동으로 생성되도록 합니다.
-
cacheDefaults 기본 캐시 설정을 지정합니다. 기능: 모든 캐시에 적용될 기본 설정을 정의합니다.
-
cacheWriter 캐시 라이터를 설정합니다. 기능: Redis와 상호작용을 처리할 RedisCacheWriter를 지정합니다.
-
enableStatistics 캐시 통계 수집을 활성화합니다. 기능: 캐시 사용 통계를 수집하여 분석할 수 있습니다.
-
initialCacheNames 초기 캐시 이름 목록을 설정합니다. 기능: 주어진 이름의 캐시들을 초기화 시킵니다.
-
transactionAware 트랜잭션을 인지하도록 설정합니다. 기능: Redis 트랜잭션 내에서 캐시 작업을 지원합니다.
-
withCacheConfiguration 특정 캐시에 대해 설정을 지정합니다. 기능: 특정 캐시 이름에 대해 고유한 설정을 적용할 수 있습니다.
-
withInitialCacheConfigurations 여러 초기 캐시 구성 정보를 설정합니다. 기능: 맵을 통해 여러 캐시 이름과 설정을 한 번에 적용할 수 있습니다.
제가 사용한 옵션 위주로 살펴 보자면
.cacheDefaults(this.createRedisCacheConfiguration(redisSerializer = redisSerializer))
@Bean
fun redisSerializer(): RedisSerializer<Any> =
GenericJackson2JsonRedisSerializer(
ObjectMapper()
.registerModules(
JavaTimeModule(),
KotlinModule.Builder().build(),
)
.setSerializationInclusion(JsonInclude.Include.ALWAYS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder().allowIfSubType(Any::class.java).build(),
ObjectMapper.DefaultTyping.EVERYTHING,
JsonTypeInfo.As.PROPERTY,
).enable(SerializationFeature.INDENT_OUTPUT),
)
private fun createRedisCacheConfiguration(
redisSerializer: RedisSerializer<Any>,
redisTtl: Long = DEFAULT_TTL,
): RedisCacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(redisTtl))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
key값과 value값을 어떤 형태로 직렬화, 역질렬화 할것인지 정의 하는 부분입니다.
다양한 구현체들이 존재하지만 여기서는 자주사용되는 몇가지만 알아보도록 합시다.
다시 GPT의 힘을 빌리자면
- StringRedisSerializer
문자열 데이터를 UTF-8 형식으로 직렬화하고 역직렬화하는 데 사용됩니다.
장점간단하고 직관적이며, Redis 명령어로 쉽게 읽고 쓸 수 있습니다. 키와 값이 문자열인 경우 적합합니다.
단점문자열 형식 이외의 데이터를 저장할 때는 적합하지 않습니다.
- JdkSerializationRedisSerializer
Java의 기본 직렬화 메커니즘인 Serializable 인터페이스를 사용하여 객체를 직렬화하고 역직렬화합니다.
장점Java 객체를 그대로 Redis에 저장할 수 있습니다. Java의 모든 직렬화 가능한 객체를 지원합니다.
단점직렬화된 데이터가 다른 플랫폼에서는 이해할 수 없는 이진 데이터로 저장됩니다.
직렬화된 데이터의 크기가 크고, 역직렬화 성능이 낮을 수 있습니다.
역직렬화 시 클래스의 호환성 문제가 발생할 수 있습니다.
- Jackson2JsonRedisSerializer
Jackson 라이브러리를 사용하여 객체를 JSON 형식으로 직렬화하고 역직렬화합니다.
장점사람이 읽을 수 있는 JSON 형식으로 데이터를 저장합니다.
다른 시스템과의 호환성이 높고, JSON을 이해할 수 있는 다양한 언어와 도구에서 쉽게 처리할 수 있습니다.
타입 정보를 포함하여 복잡한 객체도 직렬화할 수 있습니다.
단점JSON 직렬화 과정에서 속도와 크기 면에서 약간의 성능 저하가 발생할 수 있습니다.
역직렬화 시 정확한 클래스 정보를 알아야 합니다.
- GenericJackson2JsonRedisSerializer
Jackson2JsonRedisSerializer와 유사하지만, 직렬화된 JSON에 클래스 정보가 포함되도록 하여, 역직렬화 시 해당 클래스를 자동으로 인식할 수 있게 설계되었습니다.
장점객체의 클래스 정보를 유지하면서 JSON 형식으로 데이터를 저장합니다.
복잡한 객체 구조를 지원합니다.
단점클래스 정보가 추가되기 때문에 JSON의 크기가 증가할 수 있습니다.
Jackson을 기반으로 하기 때문에 기본적인 성능 이슈는 Jackson2JsonRedisSerializer와 유사합니다.
저는 key값에는 StringRedisSerializer, value값에는 GenericJackson2JsonRedisSerializer를 이용하였습니다.
현재 설정대로 redis에 어떤식으로 값이 저장되는지 간단히 보면 아래처럼 저장되는것을 볼 수 있습니다.
{
"@class" : "com.hjj.apiserver.domain.financial.FinancialProduct",
"financialProductId" : 900,
"joinRestriction" : [ "com.hjj.apiserver.domain.financial.JoinRestriction", "NO_RESTRICTION" ],
"financialProductType" : [ "com.hjj.apiserver.domain.financial.FinancialProductType", "SAVINGS" ],
"financialSubmitDay" : "202407030802",
"financialCompany" : {
"@class" : "com.hjj.apiserver.domain.financial.FinancialCompany",
"financialCompanyId" : 47,
"financialCompanyCode" : "0010453",
"dclsMonth" : "202406",
"financialGroupType" : [ "com.hjj.apiserver.domain.financial.FinancialGroupType", "SAVING_BANK" ]
}
// 이하 생략
}
다음으로
.withInitialCacheConfigurations(
mapOf(FINANCIAL_PRODUCTS to this.createRedisCacheConfiguration(redisSerializer = redisSerializer, redisTtl = 300L)),
)
이부분은 미리 특정 Cache는 TTL을 기본값이 아닌값으로 설정 할 수 있도록 작업한 부분입니다.
다음은 .transactionAware()
이부분인데 처음에 실수 했던부분이기도합니다.
redis설정 빌더에 설정이 가능하다는것을 모르고 처음에는 캐시매니저를 TransactionAwareCacheManagerProxy
로 감싸주는 형태로 작업하였더니 withInitialCacheConfigurations
에서 정의 해놓은 값들이 적용이 안되더라고요.
꼭 주의해서 사용하시기바랍니다.
기능은 트랜잭션과 함께 redis가 작동한다고 보시면돼요 간단하게 커밋이 성공했을때 -> 캐시에 적재 또는 삭제한다. 라고 생각하시면됩니다.
대략적인 redis CacheManager의 설정에 대해 알아보았는데요.
기본적인 사용하는부분은 어노테이션기반으로 사용하게 되면 1편에서 작성되어있어 그부분을 참고하시면 좋을 것 같습니다.
부족한 글 읽어주셔서 감사하며 잘못된 내용이 있거나 여러 질문은 언제든 환영합니다.