본문 바로가기
Language/Java

[Java] POJO - Cache 구현

by lucas_owner 2025. 1. 8.

 

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 과 비슷하다고 할 수 있다.

 

즉 캐시의 주요 특징은 아래와 같다.

  1. 빠른 데이터 접근: 필요 데이터에 빠르게 접근
  2. 성능 최적화: disk I/O, 네트워크 호출을 줄여 응답 시간 개선
  3. 자주 사용하는 데이터 저장: 반복적으로 자주 사용하는 데이터를 저장, 호출하는데 적합

 

2. Java(POJO) 로 Cache 구현체 구현

일반적으로 Java Module 만 단독으로 사용할 때나, 프레임워크를 사용한다면 Cache 구현체를 직접 만들어 사용할 일은 거의 없을것이다. 

다만 별도의 성능최적화 목적(튜닝), 외부 라이브러리 의존 불가 와 같은 이유가 있을때는 구현하여 사용할 수도 있다.

 

Java 로 Cache 를 구현한다면 대표적으로 Map 으로 구성하는것을 떠올릴 것이다, Cache 가 key-value 의 형태를 하고 있으니 Map 이 가장 적합할 것이다. 

다만 MapMultiThread 환경에서 안전하지 않으므로, thread-safeConcurrentHashMap 을 사용하는것이 좋은 방법이라고 사료된다.

 

해당 포스팅에서는 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. 구현

 

코드를 먼저 보고 설명은 아래에 적도록 하겠다.

class 1

해당 Cache 객체를 생성 하여 기능을 활용하게 하였다. static class 로 생성하여 사용해도 좋지만, Cache 기본 기능 구현체에 집중하기 위해 생성자를 통해 K,V 타입을 받도록 해주었다.

 

내부에는 expire time 이 필요하지만, 별도의 값을 주지 않는경우 기본 만료시간을 지정해주었다

그리고 Cache 의 데이터를 적재할 private static Class 를 정의해주었다. 내부에는 Value 와, 만료시간에 대한 객체를 정의해주었다.

 

Multi Thread 환경에서 thread-safe 하기 위해 ConcurrentHashMap 을 사용하였고,

요구사항중 스케쥴링을 통한 주기적 삭제 기능을 위해 ScheduledExecutorService 을 사용하였다.

 

이후 Main Thread 종료시 스케쥴러 service 를 shutdown 시켜주도록 하자.

 

class2

기본적으로 Cache Data 삽입은 3가지로 구분지어 줬다.

  1. key-value 저장 - 기본 expire time 지정
  2. key-value-expire time 저장
  3. expire time 없이 데이터 저장

class 3

요구사항 내용을 반영하기 위하여 Data 를 가져올때 expire time 을 비교하여, 만료되었다면 데이터를 삭제처리 하게 해주었다.

이경우 Scheduler 가 동작하기 이전이라고 상정했을때다.

 

class 4

마지막으로 생성자에서 설정한대로 스케줄러가 주기적으로 어떤 동작을 할것인지에 대한 cleanup() private Method 를 정의해주었다

expire time 이 지난 데이터들을 캐시에서 삭제하는 로직이다.

 

또한 Main Thread 가 종료될때, 스케줄러 또한 Graceful 하게 shutdown 시키기 위한 methods 이다

 


 

3. Test 

테스트는 구현한 기능을 그대로 테스트 해보도록 하겠다.

Main Class 에서 shutdown hook 을 걸어서 종료 시켜주도록 하겠다.

Test Class

 

Test Result

테스트 결과는 의도한대로 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
반응형

댓글