티스토리 뷰

반응형

spring cache  기본 사용법: https://dolgogae.tistory.com/85

spring key generator 사용법: https://dolgogae.tistory.com/86

Overview

앞선 두 개의 게시글에서 spring cache 기본 사용법과 커스텀 key generator에 대해서 살펴보았다. 마지막으로 cache를 어떻게 사용할지 결정하는 config 설정에 대해서 알아보도록 하겠다. 캐싱을 사용할 때 만료시간이 어떻게 설정할지, xml 파일로 설정을 분리할 지 어떻게 캐싱을 분산처리 할지 등에 대해서 설정이 가능하다.

CacheManager의 동작 원리

cacheManage도 여느 spring 설정과 동일하게 AOP를 기반으로 동작한다.

그리고 @EnableCaching이 있는 함수를 호출하면

[1. CacheInterceptor에서 수행을 가로챈다.]

그리고 [2. spring bean으로 등록된 cacheManager에서 해당 value의 캐시가 있나 확인]하고,

있다면 [3. cache data를 return]하고

없다면 함수 수행 후 [4. cache에 내용을 저장]하고 결과를 Return 한다.

Cache Config 기본

캐싱은 기본적으로 caching value를 찾아들어가 저장을 한다. 

@Cacheable(value = "users")
public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);
}

위에서 users를 기반으로 어떤 값이 캐싱되어 있는지를 찾는 것이다.

이 캐시를 찾기 위한 디폴트로는 ConcurrentMapCacheManager가 쓰인다. 

@Configuration
@EnableCaching
public class CacheConfig {

    @Value("#{'${cache.names}'.split(',')}")
    private List<String> cacheNames;

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager(cacheNames.toArray(new String[0]));
    }
}

위 Value는 application.yml 에 변수로 등록해주고

ConcurrentMapCacheManager를 만들어주면 가장 기본적인 사용방법이다.

 

하지만 ConcurrentMapCacheManager로는 인메모리 기반으로 성능은 보장되지만 

만료시간, 크기 제한등 추가적인 설정이 불가능하다. 주로 개발용이나 빠른 확인을 해보고 싶으면 사용하면 될듯 하다.

 

Ehcache 기반

데이터를 일정 시간 후 자동으로 만료시킬 필요가 있을 때나

대량의 데이터를 캐싱하면서도 메모리 관리를 해야 할때 ehcache는 좋은 방법이 될 수 있다.

resource 폴더 밑에 ehcache.xml 파일을 만들고, 아래와 같은 형식으로 xml 파일을 만들어주면 된다.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd">

    <!-- 디폴트 캐시 설정 (이름 없는 캐시에 적용됨) -->
    <defaultCache 
        maxEntriesLocalHeap="1000"
        eternal="false"
        timeToLiveSeconds="600"
        timeToIdleSeconds="300"
        memoryStoreEvictionPolicy="LRU"
    />

    <!-- 사용자 캐시: users 캐시 설정 -->
    <cache name="usersCache"
           maxEntriesLocalHeap="500"
           eternal="false"
           timeToLiveSeconds="3600"
           timeToIdleSeconds="1800"
           memoryStoreEvictionPolicy="LFU">
        <persistence strategy="localTempSwap"/>
    </cache>

    <!-- 사용자 캐시: products 캐시 설정 -->
    <cache name="productsCache"
           maxEntriesLocalHeap="200"
           eternal="true">
        <persistence strategy="none"/>
    </cache>
</ehcache>

 

 

위에서 쓰인 변수에 대한 설명은 아래와 같다.

  • maxEntriesLocalHeap="1000": 힙 메모리에 최대 1000개의 엔트리를 저장.
  • eternal="false": true이면 캐시 항목이 만료되지 않음 (항상 유지).
  • timeToLiveSeconds="600": 생성 시점부터 600초(10분) 후에 캐시 만료.
  • timeToIdleSeconds="300": 마지막 접근 후 300초(5분) 후에 캐시 만료.
  • memoryStoreEvictionPolicy="LRU": 메모리 초과 시 제거 정책.
    • "LRU" (Least Recently Used, 기본값)
    • "LFU" (Least Frequently Used)
    • "FIFO" (First-In-First-Out)
  • localTempSwap: 메모리 초과 시 임시 디스크 저장.
  • localRestartable: 애플리케이션 재시작 후에도 데이터 유지.

그리고 @Configuration에 아래와 같이 등록하면 ehcache 적용이 완료된다.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        net.sf.ehcache.CacheManager ehCacheManager = net.sf.ehcache.CacheManager.newInstance(
                ConfigurationFactory.parseConfiguration(getClass().getResource("/ehcache.xml"))
        );
        return new EhCacheCacheManager(ehCacheManager);
    }
}

 

 

하지만 ehcache는 기본적으로 분산 캐시로 사용되지 않으며

메모리 용량을 초과할 경우 디스크를 사용하게 되면서 성능상 문제가 생길 우려가 있다.

Redis 기반

Redis를 사용하게 되면 MSA같은 분산 환경으로 동일한 데이터를 참조할 수 있다.

추가적으로 디스크에 백업할 수 있어 더욱 안전한 환경을 제공할 수 있다.

Redis에 관한 설명은 다른 블로그나 글에 맡기도록 하겠다.

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // Redis 연결 설정 (localhost:6379)
        return new LettuceConnectionFactory("localhost", 6379);
    }

    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))  // TTL 설정 (5분)
            .disableCachingNullValues()        // null 값은 캐싱하지 않음
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 직렬화 설정

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(cacheConfig)
                .build();
    }
}

위와 같이 @Configuration을 등록하게 되면 Redis를 spring cache의 백단으로 사용이 가능하다.

주석에서 볼 수 있듯 TTL 설정이 가능하고 다른 세부 설정들이 가능하다. 

그리고 serializeValuesWith에서 설정한 값이 어떤 형식으로 Redis에 데이터를 저장할지 serializer를 넣어주는 부분이다.

Redis 기반 캐싱의 단점은 추가적인 Redis 인프라 준비가 필요하고

내 생각으로는 결국 네트워크 기반으로 Redis 데이터를 읽어오기 때문에 오버헤드가 있지 않을까 생각된다.

Caffeine 기반

caffeine은 고성능으로 캐싱을 하기 위한 방법으로 추천되고, 장단점은 대체로 ehcache와 비슷하다.

ehcache와 차이점은 좀 더 고성능을 위한 프로젝트에 더 맞다고 한다.

caffeine이 왜 고성능인지에 대한 설명은 현재 글과 주제가 맞지 않아 기회가 되면 다른 글에서 다뤄보면 좋겠다.

 

caffeine 기반 캐싱 사용방법은 우선 기본 설정과 동일하게 application.yml 파일에서 등록할 value들을 적어준다.

그리고 아래와 같이 cacheManager를 만들어준다.

@Configuration
public class CaffeineCacheConfig {

    @Value("#{'${cache.names}'.split(',')}")
    private List<String> cacheNames;

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(cacheNames.toArray(new String[0]));
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(1000)
                .recordStats());
        return cacheManager;
    }
}

 

설정 값에 대해서 좀 더 자세하게 설명하면,

Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)  // 쓰기 후 10분 만료
    .expireAfterAccess(5, TimeUnit.MINUTES); // 마지막 접근 후 5분 만료
    .maximumSize(1000); 
    .softValues();  // 메모리 부족 시 캐시 자동 제거

위 처럼 만료 시간 설정 등 다양한 설정이 가능하다. 

아니면 동적으로 캐시 이름들을 로딩하고 싶으면 Enum 기반으로 설정이 가능하다.

public enum CacheType {
    USERS("users"),
    PRODUCTS("products"),
    ORDERS("orders");

    private final String cacheName;

    CacheNames(String cacheName) {
        this.cacheName = cacheName;
    }

    public String getName() {
        return cacheName;
    }
}

@Configuration
public class CaffeineCacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(
            Arrays.stream(CacheType.values())
                  .map(CacheNames::getName)
                  .toArray(String[]::new)
        );
        
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(1000)
                .recordStats());
        return cacheManager;
    }
}

 

하지만 CaffeineCacheManager를 사용하면 각 캐시에 대해서 동일한 정책이 적용되는 단점이 있다.

이를 해결하기 위해서는 SimpleCacheManager를 이용해 서로 다른 정책을 적용하면 된다.

아래 코드는 https://velog.io/@itonse/%EA%B5%AC%ED%95%B4%EC%9C%A0-Caffeine-Cache-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EC%A0%81%EC%9A%A9를 참고 했다.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();   // SimpleCacheManager 사용
        // 각 캐시 타입에 대한 설정 적용
        List<CaffeineCache> caches = Arrays.stream(CacheType.values())   
                .map(
                        cache -> new CaffeineCache(cache.getCacheName(),
                                Caffeine.newBuilder()
                                        .expireAfterWrite(cache.getExpiredAfterWrite(), TimeUnit.SECONDS)
                                        .maximumSize(cache.getMaximumSize())
                                        .build()
                        )
                )
                .collect(Collectors.toList());
        cacheManager.setCaches(caches);

        return cacheManager;
    }
}

SimpleCacheManager에 직접 Caffeine 캐시를 등록하는 방법이다.

setCaches 좀 더 자세히 살펴보면 다양한 Cache 타입들로 설정이 가능한 것을 알 수 있다.

public void setCaches(Collection<? extends Cache> caches) {
    this.caches = caches;
}

 

 

caffeine 캐싱은 ehcache에서 얻을 수 있는 장단점과 비슷하고 추가적으로 높은 성능을 보장하는것 같다.

그리고 위 설명에서 다루지는 않았지만 비동기와 통계, refresh를 지원한다.

Caffeine이 왜 빠른가에 대해서는 앞선 설명과 같이 좀 더 공부해보고

다른 포스팅에서 설명하는게 좋을 것 같다.

https://github.com/ben-manes/caffeine/wiki/Benchmarks

 

Benchmarks

A high performance caching library for Java. Contribute to ben-manes/caffeine development by creating an account on GitHub.

github.com

또한 다른 분이 실험한 결과를 보면 Caffeine의 성능은 검증된듯 하다.

728x90
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함
250x250