본문 바로가기
spring

[Spring] EhCache를 Caffeine Cache로 변경하는 방법 (Caffeine Cache 적용 방법)

by moonsiri 2022. 6. 24.
728x90
반응형

캐시 비교 게시글은 많기 때문에 생략하겠습니다.

(Redis, EhCache, Caffeine 비교EhCache, Caffeine 비교)

 

해당 본문에서는 로컬 캐시가 EhCache로 설정되어 있는 환경을 Caffeine Cache로 변경하는 방법을 다루겠습니다.

 

 

1. pom.xml

dependency를 변경합니다.

ehcache caffeine
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.1</version>
</dependency>

 

 

2. application.yml

ehcache 설정을 제거합니다.

ehcache caffeine
spring:
  cache:
    ehcache:
      config: classpath:cache/ehcache.xml
제거

 

 

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;
    }

}

 

 

 

728x90
반응형

댓글