Spring Cache
- Spring Cache 는 cache 기능을 추상화를 지원
- EhCache, Couchbase, Redis 등의 추가적인 캐시 저장소와 빠르게 연동하여 bean으로 설정할 수 있도록 도와준다.
- 추가적인 캐시 저장소와 연결하지 않는다면, ConcurrentHashMap 기반의 Map 저장소가 자동으로 추가된다.
- 간단하게 토큰 정도만 캐시처리가 필요 한 경우 빠르게 사용할 수 있다.
- application간의 공유가 불가능하기 때문에, clustering isntance간에 공유해야 하는 경우 Redis, MemCached를 이용하는게 맞다.
Spring Cache 구성
-
dependency 추가
-
bean 설정
@EnableCaching @Configuration public class LocalCacheConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager simpleCacheManager = new SimpleCacheManager(); simpleCacheManager.setCaches(List.of( new ConcurrentMapCache("example.store1"), new ConcurrentMapCache("example.store2"))); return simpleCacheManager; } }
- @EnableCaching 을 붙여야 한다.
- 여러개의 저장소를 설정하는 경우 List로 만들어 반환하면 된다.
ConcurrentMapCache
- ConcurrentMapCache.java
public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final String name; private final ConcurrentMap<Object, Object> store; @Nullable private final SerializationDelegate serialization; public ConcurrentMapCache(String name) { this(name, new ConcurrentHashMap(256), true); } public ConcurrentMapCache(String name, boolean allowNullValues) { this(name, new ConcurrentHashMap(256), allowNullValues); } public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) { this(name, store, allowNullValues, (SerializationDelegate)null); } protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) { super(allowNullValues); Assert.notNull(name, "Name must not be null"); Assert.notNull(store, "Store must not be null"); this.name = name; this.store = store; this.serialization = serialization; } // ... }
- JDK java.util.concurrent package 코어를 기반으로 구현한 단순 cache
- ConcurrentHashMap 을 사용한다.
- ConcurrentHashMap에 경우, null을 저장할 때 내부에 미리 정의된 default object로 대체된다.
- 하지만, allowNullValues에 따라 다르게 설정될 수 있다.
- ConcurrentHashMap
- Hashtable 클래스의 단점을 보완하면서 MultiThread 환경에서 사용할 수 있도록 나온 클래스
- Hashtable 은 get, put 메소드에 synchronized로 구현되어 있어 MultiThread 환경에 적합한 듯 보이나 동시에 작업하려면 Lock을 갖기 때문에 병목현상이 발생한다.
- 동시성을 높이기 위해 나왔다
- get 메서드에는 synchronized가 존재하지 않고 put 중간에 synchronized가 존재한다.
- ConcurrentHashMap은 읽기 작업에는 여러 쓰레드가 동시에 읽을 수 있지만, 쓰기 작업에는 특정 세그먼트, 버킷에 대한 Lock을 사용
- Hashtable 클래스의 단점을 보완하면서 MultiThread 환경에서 사용할 수 있도록 나온 클래스
Service
- caching 대상 데이터
public class CacheData { private String value; private LocalDateTime expirationDate; public CacheData() {} public CacheData(String value, LocalDateTime expirationDate) { this.value = value; this.expirationDate = expirationDate; } public String getValue() {return value;} public LocalDateTime getExpirationDate() {return expirationDate;} public void setValue(String value) {this.value = value;} public void setExpirationDate(LocalDateTime expirationDate) {this.expirationDate = expirationDate;} }
- caching service
@Service public class CacheService { private static final CacheData EMPTY_DATA = new CacheData(); @Cacheable(cacheNames = "example.store1", key="#key") public CacheData getCacheData(final String key) { System.out.println(key + "에 한 값이 존재하지 않는 경우 실행"); return EMPTY_DATA; } @CachePut(cacheNames = "example.store1", key="#key") public CacheData updateCacheData(final String key, final String value) { System.out.println(key + " 에 대한 값을 " + value + "로 업데이트하는 경우 실행"); CacheData cacheData = new CacheData(); cacheData.setValue(value); cacheData.setExpirationDate(LocalDateTime.now().plusDays(1)); return cacheData; } @CacheEvict(cacheNames = "example.store1", key="#key") public boolean expireCacheData(final String key) { System.out.println(key + " 를 삭제하는 경우 실행"); return true; } public boolean isValidation(final CacheData cacheData) { return !ObjectUtils.isEmpty(cacheData) && !ObjectUtils.isEmpty(cacheData.getExpirationDate()) && StringUtils.hasText(cacheData.getValue()) && cacheData.getExpirationDate().isAfter(LocalDateTime.now()); } }
- @Cacheable
- cache를 가져오는 메서드에 붙이는 애노테이션
- cacheNames는 cache 저장소 이름(ConcurrentMapCache), value도 같은 역할을 한다.
- key에 경우 캐시 데이터가 들어있는 key 값이며, ConcurrentHashMap의 key이다
- key에 대한 value가 존재하면 기존 value가 리턴되고, getCacheData 는 수행되지 않는다.
- @CachePut
- cache 저장소(example.store1) 의 key에 대한 value 를 업데이트 할 때 사용
- @CacheEvict
- cache 저장소(example.store1) 의 key에 대한 value 를 삭제 할 때 사용
- key
- #key: SpEL(Spring Expression Language) 문법을 사용
- String, Integer, Long 등의 값은 #변수명 형태로 사용 가능
- 객체안의 멤버변수를 비교하는 경우 #객체명.멤버명 형태로 사용
- 파라미터를 보고 KeyGenerator에 의해 default key를 생성
- 파라미터가 없는 경우: 빈값
- 하나인 경우: 해당 값
- 여러 개인 경우: 모든 파라미터를 포함한 키 생성(모든 파라미터 및 해시코드 조합)
- 조건 부여
@CachePut(cacheNames="example.store1", key="#cacheData.value", condition="#cacheData.value.length() > 5") public CacheData updateCacheData(CacheData cacheData) { return cacheData; }
- cacheData에 대한 조건을 넣을 수 있다.
Test
@SpringBootTest
class CacheServiceTest {
@Autowired CacheService cacheService;
@Test
@DisplayName("cache data test")
void getCacheTest() {
String key = "testKey";
String value = "testValue";
CacheData cacheData = cacheService.getCacheData(key);
if(!cacheService.isValidation(cacheData)) {
cacheData = cacheService.updateCacheData(key, value);
}
System.out.println(cacheData.toString());
CacheData returnData = cacheService.getCacheData(key);
System.out.println(returnData.toString());
assertThat(cacheData.getValue()).isEqualTo(returnData.getValue());
assertThat(cacheData.getExpirationDate()).isEqualTo(returnData.getExpirationDate());
cacheService.expireCacheData(key);
CacheData emptyData = cacheService.getCacheData(key);
assertThat(emptyData.getValue()).isNull();
}
}
출처
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/concurrent/ConcurrentMapCache.html
- https://devlog-wjdrbs96.tistory.com/269
- https://sunghs.tistory.com/132