[Spring] ehcache 설정, 코드 기반 configuration

2025. 12. 1. 20:39·Spring

EH cache


웹 애플리케이션에서 성능을 확보하기 위해 가장 먼저, 쉽게 도입할 수 있는 기술 중 하나가 캐싱(Cache)이다. 캐시는 반복적으로 조회되는 데이터를 메모리에 보관해 DB 부하를 줄이고, 응답속도를 빠르게 만드는 도구이다. 보통 Redis부터 떠오르는데, Caffeine, EhCache와 같은 로컬 캐싱 방법도 있다. 그 중 EhCache는 안정적이고, JVM에 최적화된 로컬 캐시로 널리 사용된다.

외부 인프라(Redis 등)을 도입하지 않고, 간단하게 Spring Boot 설정만으로 캐싱을 도입할 수 있다는 장점이 있다.

 

EhCache란?

  • JVM 힙/오프힙 메모리 기반 캐싱
  • 디스크 저장 가능(옵션)
  • JSR-108(JCache) 표준 지원
  • Spring Cache 추상화(@Cacheable)와 통합되어 있음
  • 빠른 속도, 낮은 지연
  • 네트워크 비용이 없는 순수 JVM 내부 캐시

EhCache vs Redis

항목 EhCache Redis
캐시 종류 로컬 캐시 분산 캐시
저장 위치 JVM 메모리 / 디스크 외부 서버 메모리
네트워크 비용 인스턴스 내 캐시로 네트워크 비용 없음 외부 서버로 네트워크 비용발생
장애 시 서버 재시작 시 캐시 유실 Redis 장애 시 Redis에 의존하는 기능 장애 가능성
멀티 인스턴스 캐시 동기화 불가 멀티 서버간 공유 가능

 

EhCache 장단점

장점

1. 매우 빠른 속도

네트워크 왕복이 없기 때문에 Redis보다도 빠르다. 

2. 성능 안정성

기업 환경에서 오랫동안 사용된 만큼 안정적이고, 메모리 관리 정책도 성숙하다.

3. 디스크 저장 옵션

휘발성이 아니라, 스냅샷처럼 디스크 저장 기능도 지원한다.

단점

1. 클러스터 환경에서 동기화 불가

단일 인스턴스가 아니라 서버가 여러 대일 경우, 인스턴스 간 캐시 공유, 동기화가 안된다. 여러 인스턴스간 공유하는 캐시가 필요한 경우 사용할 수 없다. 반면에 Redis는 별도 서버에서 동작하기 때문에 멀티 인스턴스간 캐시 공유가 가능하다. 자주 조회되는 작은 단위의 데이터, 일회성 데이터, 동기화가 필요 없는 데이터를 캐싱할 때에 유리하다.

2. 운영 관리 어려움

Redis와 달리 모니터링/백업 등은 애플리케이션 레벨에서 직접 구현해야 한다.

3. 애플리케이션 재기동 시 캐시 초기화

디스크 옵션을 쓰지 않는 이상 애플리케이션 종료 시 캐시는 리셋된다.

 

EhCache 적용 방법 (Code)


웹에서 찾은 대부분의 소스는 아래 캡쳐본처럼 xml 기반 설정을 사용한다. 이 글에서는 Programmatic configuration 방법을 다룬다.

https://www.ehcache.org/documentation/3.3/getting-started.html

애플리케이션 버전

  • Java 21
  • Spring Boot 3.1.12

build.gradle.kts

implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.ehcache:ehcache:3.10.8:jakarta")  // Jakarta EE 버전

 

Config

config에서 모든 캐시 정보를 미리 등록해야 합니다.

import java.time.Duration;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheEventListenerConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.event.EventType;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@RequiredArgsConstructor
@EnableCaching
@Configuration
public class EhCacheConfig {

  private static final String EH_CACHE_PROVIDER_CLASS_NAME = "org.ehcache.jsr107.EhcacheCachingProvider";

  @Bean
  public CacheManager cacheManager() {
    // Cache Provider 설정
    CachingProvider provider = Caching.getCachingProvider(EH_CACHE_PROVIDER_CLASS_NAME);
    javax.cache.CacheManager jCacheManager = provider.getCacheManager();

    // 캐시 1
    CacheConfiguration<Object, Object> ehCacheConfig1 =
        CacheConfigurationBuilder.newCacheConfigurationBuilder(
                Object.class,
                Object.class,
                ResourcePoolsBuilder.newResourcePoolsBuilder()
                    .heap(100_000, EntryUnit.ENTRIES) // heap 사이즈 설정
                    .offheap(10, MemoryUnit.MB) // offheap 사이즈 설정
                //.disk(100, MemoryUnit.MB, true)
            )
            .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(10L))) // 만료 설정
            .withService(// 리스너 등록(EhCacheEventLogger)
                CacheEventListenerConfigurationBuilder
                    .newEventListenerConfiguration(
                        new EhCacheEventLogger(),
                        EventType.CREATED, EventType.UPDATED, EventType.EXPIRED, EventType.REMOVED,
                        EventType.EVICTED
                    )
                    .unordered()
                    .asynchronous()
            )
            .build();
    javax.cache.configuration.Configuration<Object, Object> config1
        = Eh107Configuration.fromEhcacheCacheConfiguration(ehCacheConfig1);
    jCacheManager.createCache("cache1", config1); // 캐시 생성
    // 캐시 2
    // 캐시 3 ...

    return new JCacheCacheManager(jCacheManager);
  }
}

 

Event Logger

import lombok.extern.slf4j.Slf4j;
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;

@Slf4j
public class EhCacheEventLogger implements CacheEventListener<Object, Object> {

  @Override
  public void onEvent(CacheEvent<?, ?> event) {
    log.info("[EHCache Event] Type={}, Key={}, Old={}, New={}",
        event.getType(), event.getKey(), event.getOldValue(), event.getNewValue());
  }
}

 

테스트, 캐시 활용 방법

Controller

@GetMapping("/default")
public SampleCacheService.User getCacheData(@RequestParam String id) {
    System.out.println("### [Controller] getCacheData 호출 id = " + id);
    return sampleCacheService.getTestUser(id);
}

Service

@Cacheable(cacheNames = "cache1", key = "#id")
public User getTestUser(String id) {
    System.out.println("### [Service] no-cache => getTestUser 실행 id = " + id);
    return new User(id, "User-" + id);
}

 

로그 확인

가독성을 위해 로그 라인은 앞쪽을 생략 후 [LOG] 로 표현합니다.

 

1. 캐시 없는 상태 - 최초 호출

API 요청 로그, Controller에서 찍은 print, Service에서 찍은 print,  캐시 생성 로그를 확인할 수 있다.

[LOG] GET /api/sample/cache/default?id=test-user1
### [Controller] getCacheData 호출 id = test-user1
### [Service] no-cache => getTestUser 실행 id = test-user1
[LOG] EhCacheEventLogger : [EHCache Event] Type=CREATED, Key=test-user1, Old=null, New=User[id=test-user1, name=User-test-user1]

 

2. 캐시 있는 상태에서 재호출

API 요청 로그와 Controller에서 찍힌 print는 확인할 수 있지만 캐시 관련 로그와 Service에서 찍는 sout은 없는 걸 볼 수 있다.

[LOG] GET /api/sample/cache/default?id=test-user1
### getCacheData 호출 id = test-user1

 

3. 캐시 만료 후 호출

Controller 호출 로그 이후에 캐시가 만료된 걸 확인하고, Service 코드 호출된 것을 확인할 수 있다.

이후 캐시를 다시 생성하는 로그도 보인다.

[LOG]GET /api/sample/cache/default?id=test-user1
### [Controller] getCacheData 호출 id = test-user1
[LOG] EhCacheEventLogger : [EHCache Event] Type=EXPIRED, Key=test-user1, Old=User[id=test-user1, name=User-test-user1], New=null
### [Service] no-cache => getTestUser 실행 id = test-user1
[LOG] EhCacheEventLogger : [EHCache Event] Type=CREATED, Key=test-user1, Old=null, New=User[id=test-user1, name=User-test-user1]

 

전체 코드 샘플

더보기

Config

EhCacheType enum을 순회하며 EhCacheConfigurationBuilder를 통해 cache 등록

import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@RequiredArgsConstructor
@EnableCaching
@Configuration
public class EhCacheConfig {

  private static final String EH_CACHE_PROVIDER_CLASS_NAME = "org.ehcache.jsr107.EhcacheCachingProvider";

  @Bean
  public CacheManager cacheManager() {
    CachingProvider provider = Caching.getCachingProvider(EH_CACHE_PROVIDER_CLASS_NAME);
    javax.cache.CacheManager jCacheManager = provider.getCacheManager();

    for (EhCacheType cacheType : EhCacheType.values()) {
      log.info("[EhCache] Cache configured name: {}", cacheType.name());
      jCacheManager.createCache(cacheType.name(), EhCacheConfigurationBuilder.build(cacheType));
    }

    return new JCacheCacheManager(jCacheManager);
  }
}

EhCacheType

heapEntrySize, offHeapSize, cahceExpireTime으로 구성

@Getter
@RequiredArgsConstructor
public enum EhCacheType {
  DEFAULT_CACHE(100_000, 10, Duration.ofMinutes(10L)),
  REFERENCE_DATA(10_000, 50, Duration.ofHours(1L));

  private final long heapEntrySize;
  private final long offHeapSize;
  private final Duration duration;

}

 

EhCacheConfigurationBuilder

CacheType에 등록된 변수들을 기반으로 EhCahceConfiguration 생성

import java.time.Duration;
import javax.cache.configuration.Configuration;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheEventListenerConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.event.EventType;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.stereotype.Component;

@Component
public class EhCacheConfigurationBuilder {

  public static CacheEventListenerConfigurationBuilder defaultListenerBuilder =
      CacheEventListenerConfigurationBuilder
          .newEventListenerConfiguration(
              new EhCacheEventLogger(),
              EventType.CREATED, EventType.UPDATED, EventType.EXPIRED, EventType.REMOVED,
              EventType.EVICTED
          )
          .unordered()
          .asynchronous();

  /**
   *
   * @param heapEntrySize ex: 100_000
   * @param offHeapSize   ex) 10MB
   * @param duration      ex) Duration.ofMinutes(1L)
   */
  public static Configuration build(long heapEntrySize,
      long offHeapSize, Duration duration) {
    CacheConfiguration<Object, Object> ehCacheConfig =
        CacheConfigurationBuilder.newCacheConfigurationBuilder(
                Object.class,
                Object.class,
                ResourcePoolsBuilder.newResourcePoolsBuilder()
                    .heap(heapEntrySize, EntryUnit.ENTRIES)
                    .offheap(offHeapSize, MemoryUnit.MB)
                //.disk(100, MemoryUnit.MB, true)
            )
            .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(duration))
            .withService(defaultListenerBuilder) // 리스너 등록
            .build();

    // JCache 변환 (Eh107Configuration)
    return Eh107Configuration.fromEhcacheCacheConfiguration(ehCacheConfig);
  }

  public static Configuration build(EhCacheType ehCacheType) {
    return build(ehCacheType.getHeapEntrySize(), ehCacheType.getOffHeapSize(),
        ehCacheType.getDuration());
  }
}

 

EhCacheEventLogger

캐시 관련 로깅을 위한 클래스

import lombok.extern.slf4j.Slf4j;
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;

@Slf4j
public class EhCacheEventLogger implements CacheEventListener<Object, Object> {

  @Override
  public void onEvent(CacheEvent<?, ?> event) {
    log.info("[EHCache Event] Type={}, Key={}, Old={}, New={}",
        event.getType(), event.getKey(), event.getOldValue(), event.getNewValue());
  }
}

 

EhCacheManager

캐시를 직접 컨트롤하기 위한 클래스

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class EhCacheManager {

  private final CacheManager cacheManager;

  /**
   * 캐시 매니저 반환
   *
   * @return CacheManager
   */
  public CacheManager getCacheManager() {
    return cacheManager;
  }

  /**
   * 캐시 반환
   *
   * @param cacheName 캐시명 (분류) EhCacheType.name()
   * @return cache 반환
   */
  public Cache getCache(String cacheName) {
    return cacheManager.getCache(cacheName);
  }

  /**
   * 캐시 수동 조회
   *
   * @param cacheName 캐시명 (분류) EhCacheType.name()
   * @param id        캐시 id
   * @param clazz     value type
   * @param <T>       cache Value type
   * @return value
   */
  public <T> Optional<T> getCache(String cacheName, Object id, Class<T> clazz) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null) {
      return Optional.empty();
    }
    T value = cache.get(id, clazz);
    return Optional.ofNullable(value);
  }

  /**
   * 캐시 전체 삭제 (모든 entry 삭제)
   *
   * @param cacheName 캐시명 (분류) EhCacheType.name()
   */
  public void clearCache(String cacheName) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache != null) {
      cache.clear();
    }
  }

  /**
   * 캐시 삭제 by id
   *
   * @param cacheName 캐시명 (분류) EhCacheType.name()
   * @param id        캐시 id
   */
  public void evictCache(String cacheName, Object id) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache != null) {
      cache.evict(id);
    }
  }

  /**
   * 캐시 업데이트
   *
   * @param cacheName 캐시명 (분류) EhCacheType.name()
   * @param id        캐시 id
   * @param value     값
   */
  public void upsertCache(String cacheName, Object id, Object value) {
    Cache cache = cacheManager.getCache(cacheName);
    if (cache != null) {
      cache.put(id, value);
    }
  }
  
}
반응형
저작자표시 (새창열림)

'Spring' 카테고리의 다른 글

카카오 로그인 API 사용 방법 - 2025 ver (Java, Spring)  (0) 2025.12.10
[Spring] 메모리 Queue + @Scheduling 배치 처리 - 부하 분산  (0) 2025.09.21
[Spring] @Transactional 전파수준 정리 - Propagation 예제코드로 테스트해보기  (0) 2025.06.19
[Spring] @RequestBody JSON 바인딩 원리와 Jackson 직렬화/역직렬화 과정 (MappingJackson2HttpMessageConverter)  (1) 2025.06.08
[JPA] fetchJoin시 MultipleBagFetchException 발생(카르테시안 곱)  (0) 2025.04.06
'Spring' 카테고리의 다른 글
  • 카카오 로그인 API 사용 방법 - 2025 ver (Java, Spring)
  • [Spring] 메모리 Queue + @Scheduling 배치 처리 - 부하 분산
  • [Spring] @Transactional 전파수준 정리 - Propagation 예제코드로 테스트해보기
  • [Spring] @RequestBody JSON 바인딩 원리와 Jackson 직렬화/역직렬화 과정 (MappingJackson2HttpMessageConverter)
HSRyuuu
HSRyuuu
Web Server Developer hsryuuu
  • HSRyuuu
    HS_dev_log
    HSRyuuu
  • 전체
    오늘
    어제
  • 링크

    • Github
    • 전체 글 보기 (248) N
      • AI (5) N
      • Spring (37)
      • Infra & DevOps (20)
      • Java (25)
      • Database (28)
      • Web & Network (14)
      • 자료구조 & 알고리즘 (30)
      • Computer Science (24)
      • Frontend (17)
        • Vue.js & Nuxt.js (9)
        • JSP_Thymeleaf (7)
      • etc (48)
        • 오픈소스 라이브러리 (5)
        • 코딩테스트 (13)
        • Trouble Shooting (7)
        • Tech Interview (6)
        • Book Review (9)
        • 끄적끄적... (6)
        • 개인 프로젝트 (2)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 태그

    트랜잭션
    기술면접
    리눅스
    SQL
    백엔드스쿨
    docker
    Linux
    클린코드
    Nuxt3
    Spring
    TechInterview
    cleancode
    백준
    자료구조
    MySQL
    JPA
    SpringBoot
    백엔드
    백엔드공부
    Java
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.4
HSRyuuu
[Spring] ehcache 설정, 코드 기반 configuration
상단으로

티스토리툴바