캐시 비교 게시글은 많기 때문에 생략하겠습니다.
(Redis, EhCache, Caffeine 비교 , EhCache, Caffeine 비교)
해당 본문에서는 로컬 캐시가 EhCache로 설정되어 있는 환경을 Caffeine Cache로 변경하는 방법을 다루겠습니다.
1. pom.xml
dependency를 변경합니다.
ehcache | caffeine |
|
|
2. application.yml
ehcache 설정을 제거합니다.
ehcache | caffeine |
|
제거 |
3. Configuration
비어 있는 EhCacheConfiguration과 달리 CaffeineCacheConfiguration에서는 ehcache.xml에 설정되어있는 cache를 추가하겠습니다.
3.1. EhCacheConfiguration 제거
@EnableCaching
@Configuration
public class EhCacheConfiguration {
}
3.2. CaffeineCacheConfiguration 생성
@EnableCaching
@Configuration
public class CaffeineCacheConfiguration {
@Bean
public Caffeine caffeineConfig() {
return Caffeine
.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(60, TimeUnit.SECONDS);
}
@Bean
public CacheManager cacheManager(Caffeine caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
위 설정은 간단하게 Caffeine 설정하는 방법이고, 우리는 기존 ehcache 로직을 caffeine으로 변경해야 하기 때문에 ehcache.xml을 대체하는 방법을 알아보겠습니다.
아래는 기존 ehcache.xml 설정입니다.
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false">
<cache
name="commonCache"
eternal="false"
maxElementsInMemory="64"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="20"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="userInfoCache"
eternal="false"
maxElementsInMemory="500"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="deptInfoCache"
eternal="false"
maxElementsInMemory="200"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
ehcache.xml 대신에 Caffeine에서 사용할 캐시를 정의하기 위해 enum class를 생성하겠습니다.
@Getter
@AllArgsConstructor
public enum CacheTypeCd {
COMMON_CACHE("commonCache", 64, 20, TimeUnit.SECONDS),
USER_INFO_CACHE("userInfoCache", 500, 10, TimeUnit.MINUTES),
DEPT_INFO_CACHE("deptInfoCache", 200, 10, TimeUnit.MINUTES),
;
private String cacheName;
private long maximumSize;
private long duration; // ttl
private TimeUnit timeUnit;
}
캐시를 정의할 enum class를 생성한 이유는 cacheManager에 하나하나 등록해도 되지만 ehcache.xml처럼 캐시를 정의하여 관리하기 위해서입니다.
@EnableCaching
@Configuration
public class CaffeineCacheConfiguration {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// @formatter:off
List<CaffeineCache> caches = Arrays.stream(CacheTypeCd.values())
.map(cacheType -> {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats()
.maximumSize(cacheType.getMaximumSize())
.expireAfterWrite(cacheType.getDuration(), cacheType.getTimeUnit());
return new CaffeineCache(cacheType.getCacheName(), caffeine.build());
}).collect(toList());
// @formatter:on
cacheManager.setCaches(caches);
return cacheManager;
}
}
이렇게만 하면 기존 사용하던 @Cacheable은 수정할 필요 없이 ehcache에서 caffeine으로 변경할 수 있습니다.
4. cache 정보 조회
/**
* cache의 정보들을 조회
*/
@ResponseBody
@PostMapping("/localCache/details")
public ResponseEntity<Map<String, ConcurrentMap<Object, Object>>> localCacheValueInfo() {
Collection<String> cacheNames = cacheManager.getCacheNames();
Map<String, ConcurrentMap<Object, Object>> response = new HashMap<>(cacheNames.size());
for (String cacheName : cacheNames) {
CaffeineCache cache = (CaffeineCache) cacheManager.getCache(cacheName);
if (cache != null) {
Cache<Object, Object> nativeCache = cache.getNativeCache();
response.put(cacheName, nativeCache.asMap());
}
}
return ResponseEntity.ok(response);
}
/**
* cache의 키 상태 정보들을 조회
*/
@ResponseBody
@PostMapping("/localCache/status")
public ResponseEntity<List<Map<String, String>>> localCacheStatInfo() {
Collection<String> cacheNames = cacheManager.getCacheNames();
List<Map<String, String>> response = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
CaffeineCache cache = (CaffeineCache) cacheManager.getCache(cacheName);
if (cache != null) {
Cache<Object, Object> nativeCache = cache.getNativeCache();
response.add(Map.of(cacheName, nativeCache.stats().toString()));
}
}
return ResponseEntity.ok(response);
}
/**
* cache의 모든 캐시를 제거
*/
@ResponseBody
@PostMapping("/localCache/clearAll")
public ResponseEntity<HashMap> clearAllCache() {
List<String> cacheNameList = new LinkedList<>();
for (String cacheName : cacheManager.getCacheNames()) {
cacheNameList.add(cacheName);
Objects.requireNonNull(cacheManager.getCache(cacheName)).clear();
}
HashMap<String, Object> rtnMap = new HashMap<>();
rtnMap.put("msg", "success remove all cache");
rtnMap.put("removedCacheNameList", cacheNameList);
return ResponseEntity.ok(rtnMap);
}
/**
* 특정 캐시 1건을 clear
*/
@PostMapping("/localCache/clear")
public ResponseEntity<String> clearTargetCache(String cacheName) {
try {
Objects.requireNonNull(cacheManager.getCache(cacheName)).clear(); //해당 캐시 clear
return ResponseEntity.ok(String.format("'%s' clear success.", cacheName));
} catch (Exception e) {
log.error("Exception msg:{} | stack trace:", e.getMessage(), e);
return new ResponseEntity<>(String.format("'%s' cache clear FAIL", cacheName), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
번외) RefreshAfterWrite
사실 caffeine으로 변경한 이유 중 하나가 refresh 기능을 사용하기 위해서입니다.
Caffeine은 expireAfterWrite 말고 refreshAfterWrite 기능도 제공하고있습니다. 해당 기능을 사용하기 위해 caffeine 설정을 수정하겠습니다.
1. 어떤 캐시 전략을 사용할 것인지 구분하기위한 enum 생성
public enum CacheRefreshStrategyCd {
REFRESH, // refreshAfterWrite
EXPIRE, // expireAfterWrite
;
}
2. Refresh 전략을 사용하기위한 설정값 추가
@Getter
public enum CacheTypeCd {
COMMON_CACHE("commonCache", 64, 20, TimeUnit.SECONDS, CacheRefreshStrategyCd.EXPIRE, null, null),
USER_INFO_CACHE("userInfoCache", 500, 10, TimeUnit.MINUTES, CacheRefreshStrategyCd.REFRESH, (param) -> {
UserDAO userDAO = BeanUtils.getBean(UserDAO.class);
// public static final ObjectMapper OM = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
UserDTO.Req castParam = OM.convertValue(param, UserDTO.Req.class);
return userDAO.selectOneWithoutLocalCache(castParam);
}, Executors.newFixedThreadPool(1)),
DEPT_INFO_CACHE("deptInfoCache", 200, 10, TimeUnit.MINUTES, CacheRefreshStrategyCd.EXPIRE, null, null),
;
private String cacheName;
private long maximumSize;
private long duration; // ttl
private TimeUnit timeUnit;
private CacheRefreshStrategyCd strategyCd;
private Function<Object, Object> dataLoadFunction;
private Executor executor;
CacheTypeCd(String cacheName, long maximumSize, long duration, TimeUnit timeUnit, CacheRefreshStrategyCd strategyCd, Function<Object, Object> dataLoadFunction, Executor executor) {
this.cacheName = cacheName;
this.maximumSize = maximumSize;
this.duration = duration;
this.timeUnit = timeUnit;
this.strategyCd = strategyCd;
if (CacheRefreshStrategyCd.REFRESH.equals(strategyCd)) {
this.dataLoadFunction = Objects.requireNonNull(dataLoadFunction);
this.executor = Objects.requireNonNullElseGet(executor, () -> Executors.newFixedThreadPool(1));
}
}
}
dataLoadFunction은 캐시가 reload될때 조회할 메서드 function을 정의합니다.
public class UserDTO {
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode // 객체비교 시 사용
public static class Req {
// ...
}
@Component
public class BeanUtils {
private static GenericApplicationContext genericApplicationContext;
public BeanUtils(GenericApplicationContext genericApplicationContext) {
BeanUtils.genericApplicationContext = genericApplicationContext;
}
public static <T> T getBean(Class<T> clz) {
return genericApplicationContext.getBean(clz);
}
public static <T> T getBean(String beanName, Class<T> clz) {
return genericApplicationContext.getBean(beanName, clz);
}
}
@Repository
public class UserDAO {
private static final String NAMESPACE = "user.";
@Resource
private SqlSession sqlSession;
/**
* 유저정보 조회
*/
@Cacheable(cacheNames = "userInfoCache", key = "#p0")
public UserVO selectOne(UserDTO.Req param) {
return sqlSession.selectOne(NAMESPACE + "selectOne", param);
}
/**
* 로컬 캐시없이 유저정보 조회 (caffeine cache refresh용)
*/
public UserVO selectOneWithoutLocalCache(UserDTO.Req param) {
return sqlSession.selectOne(NAMESPACE + "selectOne", param);
}
}
3. Refresh 전략 추가
@Slf4j
@EnableCaching
@Configuration
public class CaffeineCacheConfiguration {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// @formatter:off
List<CaffeineCache> caches = Arrays.stream(CacheTypeCd.values())
.map(cacheType -> {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats().maximumSize(cacheType.getMaximumSize());
switch (cacheType.getStrategyCd()) {
case EXPIRE:
caffeine.expireAfterWrite(cacheType.getDuration(), cacheType.getTimeUnit());
return new CaffeineCache(cacheType.getCacheName(), caffeine.build());
case REFRESH:
caffeine.refreshAfterWrite(cacheType.getDuration(), cacheType.getTimeUnit())
.executor(cacheType.getExecutor());
return new CaffeineCache(cacheType.getCacheName(), caffeine.buildAsync(key -> cacheType.getDataLoadFunction().apply(key)).synchronous());
default:
throw new IllegalArgumentException("Not support cache refresh strategy. | request strategy : " + cacheType.getStrategyCd());
}
}).collect(toList());
// @formatter:on
cacheManager.setCaches(caches);
return cacheManager;
}
}
'spring' 카테고리의 다른 글
[Spring] RequestContextHolder로 HttpServletRequest 가져오기 (0) | 2022.07.21 |
---|---|
[Spring/Jenkins] 젠킨스 Rest API로 젠킨스 Job 실행 (0) | 2022.07.04 |
[Spring] HttpStatus.LOCKED : 423 Client Error (0) | 2022.06.03 |
[Spring] AOP와 @EnableAspectJAutoProxy (0) | 2022.04.27 |
[Spring] java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTags (0) | 2021.11.30 |
댓글