Hystrix에 대해 알고 싶으신 분은 이전 포스팅을 확인해주세요.
회사에서 서비스를 구현하며 조회에 대한 Redis 캐시를 적용하기 위해 CacheManager를 재정의 하였습니다.
@Bean
@Primary
CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager.RedisCacheManagerBuilder redisCacheManagerBuilder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory);
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(10))
.computePrefixWith(...));
return new redisCacheManagerBuilder.cacheDefaults(config).build();
}
RedisCacheManager를 적용한 후 Redis의 장애로 인해 서비스가 영향을 받을 수도 있을 것이란 생각이 들어 RedisCacheManager에 Hystrix를 적용해보겠다는 다짐을 하게되었고,
이를 구현함으로써 애플리케이션은 Redis Cache에 대해 장애 내성을 가질 수 있게 되었습니다.
오늘은 RedisCacheManager에 Hystrix를 어떻게 적용했는지에 대해 공유해보고자 합니다.
public class HystrixCacheManager implements CacheManager {
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
private final CacheManager cacheManger;
public HystrixCacheManager(CacheManager cacheManger){
this.cacheManger = cacheManger;
}
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, key -> new HystrixCache(cacheManger.getCache(key)));
}
@Override
public Collection<String> getCacheNames() {
return cacheManger.getCacheNames();
}
}
우선 HystrixCacheManager는 Proxy 패턴을 사용하고 있는 것을 볼 수 있습니다.
기존 getCacheNames() 메소드는 별다른 처리 없이 주입받은 CacheManager의 기능을 그대로 사용하고 있지만 getCache(String name) 메소드는 기존 캐시를 HystrixCache로 감싸서 처리하고 있는 모습을 볼 수 있습니다.
이때 기존 캐시에 존재하지 않을 경우 cacheMap에 새로운 HystirxCache를 생성하여 넣어주는 것을 볼 수 있는데 동시성 처리를 위해 ConcurrentHashMap을 사용하였습니다.
public class HystrixCache implements Cache {
private final Cache cache;
public HystrixCache(Cache cache) {
this.cache = cache;
}
@Override
public String getName() {
return cache.getName();
}
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
return new HystrixGetCommand(cache, key).execute();
}
@Override
public <T> T get(Object key, Class<T> type) {
return cache.get(key, type);
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return cache.get(key, valueLoader);
}
@Override
public void put(Object key, Object value) {
new HystrixPutCommand(cache, key, value).execute();
}
@Override
public void evict(Object key) {
new HystrixEvictCommand(cache, key).execute();
}
@Override
public void clear() {
cache.clear();
}
}
위 코드 또한 Proxy 패턴을 사용한 것을 볼 수 있는데, 이 코드에서 중요하게 봐야하는 부분은 바로 get(Object key), put(Object key, Object value), evict(Object key) 메소드 3가지 입니다.
위 메소드를 보면 해당 기능을 HystrixCommand로 감싸서 처리하는 모습을 볼 수 있습니다.
public class HystrixGetCommand extends HystrixCommand<ValueWrapper> {
private final Cache cache;
private final Object key;
protected HystrixGetCommand(Cache cache, Object key) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("hystrix-get"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.defaultSetter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerRequestVolumeThreshold(5)));
this.cache = cache;
this.key = key;
}
@Override
protected ValueWrapper run() throws Exception {
return cache.get(key);
}
@Override
protected ValueWrapper getFallback() {
return null;
}
}
public class HystrixPutCommand extends HystrixCommand<Object> {
private final Cache cache;
private final Object key;
private final Object value;
public HystrixPutCommand(Cache cache, Object key, Object value) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("hystrix-put"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.defaultSetter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerRequestVolumeThreshold(5)));
this.cache = cache;
this.key = key;
this.value = value;
}
@Override
protected Object run() throws Exception {
cache.put(key, value);
return null;
}
@Override
protected Object getFallback() {
return null;
}
}
public class HystrixEvictCommand extends HystrixCommand<Object> {
private final Cache cache;
private final Object key;
public HystrixEvictCommand(Cache cache, Object key) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("hystrix-evict"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.defaultSetter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerRequestVolumeThreshold(5)));
this.cache = cache;
this.key = key;
}
@Override
protected Object run() throws Exception {
cache.evict(key);
return null;
}
@Override
protected Object getFallback() {
return null;
}
}
마지막으로 CacheManager 설정만 해주시면 HystrixCacheManager 사용 준비가 모두 완료됩니다.
@Bean
@Primary
CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager.RedisCacheManagerBuilder redisCacheManagerBuilder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory);
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(10))
.computePrefixWith(...));
return new HystrixCacheManager(redisCacheManagerBuilder.cacheDefaults(config).build());
}