게시판 즐겨찾기
편집
드래그 앤 드롭으로
즐겨찾기 아이콘 위치 수정이 가능합니다.
[Java] 캐시 클래스.
게시물ID : programmer_23282짧은주소 복사하기
작성자 : 봄아
추천 : 4
조회수 : 1388회
댓글수 : 3개
등록시간 : 2021/09/03 17:14:25

백엔드 작업 하면서 내부 변동성 없는 데이터를 DB를 통해 아주 자주~ 자주 호출 하는 구분이 생겼다.

캐싱 어노테이션을 이용해 보려 했지만 그낙 내 맘에 들지는 않았다.

일정 시간이 지나면 원하는 것만 캐시 내용이 제거 됬으면 하는 내 소망이였다. 그래서 만들었다. 



import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 일정 시간 지나면 항목 제거 Map<P>
 * 항목을 추가하면 내부에 runnable이 생성되고 interval 시간에 삭제 처리가 이뤄 진다.
 * @param <K>
 * @param <V>
 * @see {@link RemoveEventListener}
 */
public class CacheIntervalMap<K, V> extends ConcurrentHashMap<K, V>{

    private static final long serialVersionUID = -3116825266165827645L;
    
    /** 스케줄러 서비스 */
    private final ScheduledExecutorService scheduleExe;
    
    /** 지속 시간 */
    private final long interval;
    
    /** 지속 시간 단위 */
    private final TimeUnit unit;
    
    /** 자동 삭제하기 전 문의하기 이벤트 리스너 */
    private final RemoveEventListener<K, V> listener;

    /** 중간 취소 처리 위한 관리 객체 */
    private final Map<K, Future<K>> callManage;
    
    /**
     * 일정 시간이 지나면 제거
     * @param <K>
     * @param <V>
     */
    private static class RemoverCashIntervalMap<K, V> implements Callable<K>{
        
        private final CacheIntervalMap<K, V> owner;
        
        private final K key;
        
        public RemoverCashIntervalMap(CacheIntervalMap<K, V> owner, K key) {
            this.owner = owner;
            this.key = key;
        }
        
        @Override
        public K call() throws Exception {

           System.out.println("자동 삭제 " + this.key );

            if( ! this.owner.containsKey(this.key) )
                return this.key;
            
            // 일단 지우고 나서 리스너로 문의해 본다.
            final V value = this.owner.remove(this.key);
            
            if( this.owner.listener == null )
                return this.key;
            
            if( ! this.owner.listener.isRemove(this.key, value) ) {
                // 이미 삭제된거 어쩔 수 없다. 다시 추가~!
                this.owner.put(this.key, value);
            }
            
            return this.key;
        }
    }
    
    /**
     * 삭제하기
     * @param interval 일정시간
     * @param unit 시간 단위
     */
    public CacheIntervalMap(long interval, TimeUnit unit) {
        this(interval, unit, null);
    }
    
    /**
     * 삭제하기
     * @param interval 일정시간
     * @param unit 시간 단위
     * @param listener 삭제할지 말지 결정.
     */
    public CacheIntervalMap(long interval, TimeUnit unit, RemoveEventListener<K, V> listener) {
        super();
        this.interval = interval;
        this.unit = unit;
        this.listener = listener;
        
        this.scheduleExe = Executors.newSingleThreadScheduledExecutor();
        this.callManage = new ConcurrentHashMap<K, Future<K>>();
    }
    
    @Override
    public V remove(Object key) {
        this.callManage.remove( key ).cancel(false);
        return super.remove(key);
    }
    
    @Override
    public V put(K key, V value) {
        
        this.callManage.put(key, this.scheduleExe.schedule( new RemoverCashIntervalMap<K,V>(this, key), interval, unit) );
        
        return super.put(key, value);
    }
    
    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        
        for( K key : m.keySet() ) {
            this.callManage.put(key, this.scheduleExe.schedule( new RemoverCashIntervalMap<K,V>(this, key), interval, unit) );
        }
        
        super.putAll(m);
    }
    
    @Override
    public void clear() {
        
        for( Future<K> k : this.callManage.values() ) {
            k.cancel(true);
        }
        
        this.callManage.clear();
        
        super.clear();
    }
    
}

이벤트 클래스

 /**
 * 삭제 되기 전 발생하는 이벤트
 * @param <K>
 * @param <V>
 * @see {@link CacheIntervalMap}
 */
public interface RemoveEventListener<K, V> {

    /**
     * 삭제 되기전 문의 하기~
     * @param key
     * @param value
     * @return true: 삭제해라. false:다음에 삭제할까 생각해 볼께.
     */
    public boolean isRemove(K key, V value);
}


사용 예제...
 
final Map<String, Integer> testExe = new CacheIntervalMap<String, Integer>(5, TimeUnit.SECONDS, new RemoveEventListener<String, Integer>() {
            
            @Override
            public boolean isRemove(String key, Integer value) {
                // BB는 삭제하지 않는다.
                return !"BB".equals(key);
            }
            
        });
        
        testExe.put("AA", 100);
        testExe.put("BB", 200);
        testExe.put("CC", 300);


결과

자동 삭제 AA
자동 삭제 BB
자동 삭제 CC
자동 삭제 BB
자동 삭제 BB
자동 삭제 BB

5초마다 계속 삭제 시도는 하지만 BB는 지워지지 않는다.

뭐 ... 아주 많은 데이터를 구겨 넣으면 문제가 되겠지만 몇 백개의 작은 객체는 충분히 버틸만 하다.
 
출처 내 머리.
전체 추천리스트 보기
2021-09-04 09:19:58추천 0
마지막 이슈는 사용되는 오브젝트의 캐시 힛 패턴과 메모리 사이즈를 고려하여 오브젝트 수의 upper bound 를 정하고 LRU, LFU, FIFO 와 같은 로직을 넣어도 좋을것 같네요

또는 캐시를 함수 ( Fx = y ) 라 생각하면 F=로직 프로세스, x=입력, y=출력의 다양한 관점으로 캐시를 할 수 있기 때문에 이런 부분들도 고려하시면 더욱 다양한 시나리오를 효율적으로 커버할 수 있는 캐시 클래스가 될것같습니다

그리고 중앙화된 캐시서버 없이 캐시로직이 서버 인스턴스 별로 적용이 된다면 여러개의 서버 인스턴스를 띄웟을 경우 각 인스턴스가 동일한 키에 대해 각자 다른 데이터를 캐시할 수 있기 때문에 필요에 따라 캐시 데이터 동기화 로직도 필요할 수 있습니다. 이런 이슈는 DB 엔진쪽에 데이터 싱크를 맞추기 위한 알고리즘들이 다양한데 이 부분을 참고하시면 도움이 될 것 같아요

개인적인 경험으로는 이렇게 필요에 의해 캐시를 직접 구현해보고 생각지 못햇던 또 다른 이슈들에 부딪히고 파고드는 것들이 다양한 개발상황에서 효율적인 방법 생각하는데 많은 도움이 되었던것 같습니다.
댓글 1개 ▲
2021-09-04 12:23:45추천 0
네... 뎃글 고마워요.
안 그래도 그놈의 효율과 동기화 때문에.. ㅠㅠ 이걸 운영에 사용할까 말까 고민중이에요.
그래서 redis라는걸 요즘 테스트 중입니다.
redis 결과가 좋으면 웹소켓 서비스에도 붙여 보려구요. ㅎㅎ 잘 됐으면 좋겠네요~
2021-09-11 07:07:45추천 1
댓글 0개 ▲
[본인삭제]드로이드_안
2021-09-11 14:42:24추천 0
댓글 0개 ▲
새로운 댓글이 없습니다.
새로운 댓글 확인하기
글쓰기
◀뒤로가기
PC버전
맨위로▲
공지 운영 자료창고 청소년보호