Java(POJO) Cache 구현
개발을 하다보면 Cache, Caching 과 같은 단어들을 들어보고는 했을것이다.
Spring framework 의 경우 기본적으로 캐싱을 지원하는 기능들을 지원한다, 또한 다양한 캐시 구현체들을 제공한다(Redis, Encache, Caffenine) 해당 구현체들로 더 많은 기능들을 사용할 수 있다.
1. Cache(캐시)란?
그렇다면 Cache(캐시)란 무엇일까?
Cache 는 데이터에 빠르게 접근하기위해 빠른 저장공간(Memory)을 활용하는 기술이다.
Application 에서 자주 사용하는 데이터를 Memory 와 같이 빠른 저장소에 데이터를 임시로 저장하고,
이후 데이터가 필요할 때 캐시 데이터에 접근하여 바로 사용한다.
캐시에서 데이터를 가져오는 이유는 DB나, Disk 와 같은 저장소는 Memory 보다 속도가 느리고, 비용이 많이 들기 때문이다.
Cache 는 Key-Value 의 형태를 띠고 있으며 Java 의 Map 과 비슷하다고 할 수 있다.
즉 캐시의 주요 특징은 아래와 같다.
- 빠른 데이터 접근: 필요 데이터에 빠르게 접근
- 성능 최적화: disk I/O, 네트워크 호출을 줄여 응답 시간 개선
- 자주 사용하는 데이터 저장: 반복적으로 자주 사용하는 데이터를 저장, 호출하는데 적합
2. Java(POJO) 로 Cache 구현체 구현
일반적으로 Java Module 만 단독으로 사용할 때나, 프레임워크를 사용한다면 Cache 구현체를 직접 만들어 사용할 일은 거의 없을것이다.
다만 별도의 성능최적화 목적(튜닝), 외부 라이브러리 의존 불가 와 같은 이유가 있을때는 구현하여 사용할 수도 있다.
Java 로 Cache 를 구현한다면 대표적으로 Map 으로 구성하는것을 떠올릴 것이다, Cache 가 key-value 의 형태를 하고 있으니 Map 이 가장 적합할 것이다.
다만 Map 은 MultiThread 환경에서 안전하지 않으므로, thread-safe 한 ConcurrentHashMap 을 사용하는것이 좋은 방법이라고 사료된다.
해당 포스팅에서는 Multi Thread 환경에서 안전한 방법으로 진행 하도록 하겠다.
2-1. 설계
Cache 구현체 필요 기능과 요구사항을 먼저 정리하도록 하겠다.
1. Key-value 형태의 data 적재 필요
2. Key, Value 는 사용자가 원하는 타입을 지정하여 넣을 수 있어야 한다.
3. Expire Time 기능을 추가하여 사용자 지정시간, 혹은 기본 지정시간이 지나면 삭제 처리되어야 한다.
4. cache size 확인, clear, delete 기능 필요
이때 3번요구사항의 Expire time 이 지났을 경우 삭제는 2가지 방식으로 진행할 예정이다.
1. 캐시 조회시 expire time 을 지났으면 삭제
2. 스케쥴링을 통해 주기적으로 확인 후 삭제
2-2. 구현
코드를 먼저 보고 설명은 아래에 적도록 하겠다.
해당 Cache 객체를 생성 하여 기능을 활용하게 하였다. static class 로 생성하여 사용해도 좋지만, Cache 기본 기능 구현체에 집중하기 위해 생성자를 통해 K,V 타입을 받도록 해주었다.
내부에는 expire time 이 필요하지만, 별도의 값을 주지 않는경우 기본 만료시간을 지정해주었다
그리고 Cache 의 데이터를 적재할 private static Class 를 정의해주었다. 내부에는 Value 와, 만료시간에 대한 객체를 정의해주었다.
Multi Thread 환경에서 thread-safe 하기 위해 ConcurrentHashMap 을 사용하였고,
요구사항중 스케쥴링을 통한 주기적 삭제 기능을 위해 ScheduledExecutorService 을 사용하였다.
이후 Main Thread 종료시 스케쥴러 service 를 shutdown 시켜주도록 하자.
기본적으로 Cache Data 삽입은 3가지로 구분지어 줬다.
- key-value 저장 - 기본 expire time 지정
- key-value-expire time 저장
- expire time 없이 데이터 저장
요구사항 내용을 반영하기 위하여 Data 를 가져올때 expire time 을 비교하여, 만료되었다면 데이터를 삭제처리 하게 해주었다.
이경우 Scheduler 가 동작하기 이전이라고 상정했을때다.
마지막으로 생성자에서 설정한대로 스케줄러가 주기적으로 어떤 동작을 할것인지에 대한 cleanup() private Method 를 정의해주었다
expire time 이 지난 데이터들을 캐시에서 삭제하는 로직이다.
또한 Main Thread 가 종료될때, 스케줄러 또한 Graceful 하게 shutdown 시키기 위한 methods 이다
3. Test
테스트는 구현한 기능을 그대로 테스트 해보도록 하겠다.
Main Class 에서 shutdown hook 을 걸어서 종료 시켜주도록 하겠다.
테스트 결과는 의도한대로 Expire Time 이후에는 null 값을 return 한다.
각 기능들도 동작을 확인했고, ShutDown 또한 확인 되었다.
해당 Class 를 더욱 확장하여, Spring Framework 의 @Cachable 과 같이 편하게 사용할 수 있도록 구현하여 사용하면
더욱 편하게 사용할 수 있을것 같다. 또한 cleanup() 의 로직을 최적화를 한다던가, 성능튜닝에 대한 부분을 좀더 고려해보면 좋을것 같다.
전체 코드
package com.lucas.cache;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @package : com.lucas.cache
* @name : ThreadSafeWithExpCache.java
* @date : 2025. 1. 8. 오후 3:04
* @author : lucaskang(swings134man)
* @Description: Multi Thread 환경에서 Safe 하고 Expire 기능을 가진 Cache 구현체
**/
public class ThreadSafeWithExpCache<K, V> {
/* ---------------- Constants -------------- */
/**
* Default Expire Time: 1 hour
*/
private static final long DEFAULT_EXPIRE_TIME = 60 * 60 * 1000;
/* ---------------- Objects -------------- */
private static class CacheItem<V> {
final V value;
final Long expireTime; // if null then never expire Cache
/**
* All Value Constructor
* @param value Cache Value {@code V}
* @param expireTime Expire Time in {@code Milliseconds}
*/
CacheItem(V value, Long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
/**
* Constructor without Expire Time
* @param value Cache Value {@code V}
*/
CacheItem(V value) {
this(value, null);
}
}
private final ConcurrentHashMap<K, CacheItem<V>> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler;
/* ---------------- Constructors -------------- */
/**
* Default Class Constructor - Create Scheduler with 1 Thread
*/
public ThreadSafeWithExpCache() {
this.scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::cleanup, 30* 1000, 30 & 1000, TimeUnit.MILLISECONDS); // 30s cycle cleanup
}
/* ---------------- Public operations -------------- */
/**
* Put Cache Item with Default Expire Time
*
* Default Expire Time is 1 hour
*
* @param key Cache Key {@code K}
* @param value Cache Value {@code V}
* @throws NullPointerException if {@code key}, {@code value} is null
* @author : lucaskang(swings134man)
*/
public void put(K key, V value) {
if(key == null || value == null)
throw new NullPointerException();
long expireTime = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;
cache.put(key, new CacheItem<>(value, expireTime));
}
/**
* Put Cache Item with Expire
* @param key Cache Key {@code K}
* @param value Cache Value {@code V}
* @param expireTime Expire Time in {@code Milliseconds}
* @throws NullPointerException if {@code key}, {@code value} is null, or @{code expireTime < 0}
* @author : lucaskang(swings134man)
*/
public void put(K key, V value, long expireTime) {
if(key == null || value == null || expireTime < 0)
throw new NullPointerException();
long expireTimeMillis = System.currentTimeMillis() + expireTime;
cache.put(key, new CacheItem<>(value, expireTimeMillis));
}
/**
* Put Cache Item without Expire
* @param key Cache Key {@code K}
* @param value Cache Value {@code V}
* @throws NullPointerException if {@code key}, {@code value} is null
* @author : lucaskang(swings134man)
*/
public void putWithoutExpire(K key, V value) {
if(key == null || value == null)
throw new NullPointerException();
cache.put(key, new CacheItem<>(value));
}
/**
* Get Cache Item by Key
* @param key Cache Key
* @return if Cache Item is Exist and not Expired return {@code value}
* @throws NullPointerException if key is null
* @author : lucaskang(swings134man)
*/
public V get(K key) {
if(key == null)
throw new NullPointerException();
CacheItem<V> cacheItem = cache.get(key);
if(cacheItem == null)
return null;
if(cacheItem.expireTime != null && cacheItem.expireTime < System.currentTimeMillis()) {
cache.remove(key);
return null;
}
return cacheItem.value;
}
/**
* Check if the Cache contains the Key
* @param key Cache Key
* @return {@code true} : if this com.lucas.cache contains the key And not Expired
* @author : lucaskang(swings134man)
*/
public boolean containsKey(K key) {
CacheItem<V> cacheItem = cache.get(key);
if(cacheItem == null)
return false;
return cacheItem.expireTime == null || cacheItem.expireTime >= System.currentTimeMillis();
}
/**
* Remove Cache Item by Key
* @param key Cache Key
* @author : lucaskang(swings134man)
*/
public void remove(K key) {
cache.remove(key);
}
/**
* Clear All Cache Items
* @author : lucaskang(swings134man)
*/
public void clear() {
cache.clear();
}
/**
* return Cache Size
* @return {@code int} : Cache Size
* @author : lucaskang(swings134man)
*/
public int size() {
return cache.size();
}
/**
* Shutdown the Scheduler
* @author : lucaskang(swings134man)
*/
public void shutdown() {
scheduler.shutdown();
System.out.println("Cache Scheduler Shutdown!");
}
/* ---------------- Private operations -------------- */
/**
* Clean up Expired Cache Items
* @author : lucaskang(swings134man)
*/
private void cleanup() {
long now = System.currentTimeMillis();
cache.forEach((key, cacheItem) -> {
if(cacheItem.expireTime != null && cacheItem.expireTime < now) {
cache.remove(key);
}
});
}
}//end of class
'Language > Java' 카테고리의 다른 글
[Java & Spring] Version 비교 방법 - version4j (2) | 2024.06.25 |
---|---|
[Java] var 키워드란? (간단예제포함) (0) | 2023.09.26 |
[Java] ThreadLocal이란? - (ThreadLocal, InheritableThreadLocal) 설명 및 예제(테스트) (0) | 2023.03.07 |
[Java] Java 메모리 영역(stack, heap, static), JVM, JAVA 변수 종류 (2) | 2023.02.07 |
[Java] Java 컬렉션(Collection) (0) | 2023.02.01 |
댓글