예전 CommonDaoPattern 이라고 하여, 프로세스는 다음과 같습니다.

  1. ehcache를 검색합니다. 2-1. ehcache에 데이터가 없을 시 레디스를 검색합니다. 2-2. ehcache에 데이터가 있을 시 ehcache의 데이터를 return 합니다. 3-1. redis에 데이터가 없을 시 DB를 검색합니다. 3-2. redis에 데이터가 있을 시 ehcache에 해당 데이터를 넣고 데이터를 return 합니다. 4-1. DB에 검색한 데이터가 없을 시 5분간 해당 primary key를 캐시에 담아 return null 시킵니다. 4-2. DB에 검색한 데이터가 있을 시, 레디스랑 ehcache에 해당 데이터를 담습니다.
public class CommonDaoPattern<T1> {
	private static final Logger LOG = LoggerFactory.getLogger(CommonDaoPattern.class);
	protected static final Random random = new Random();
	private CommonEHCacheManager<T1> cacheManager;
	private CommonRedisManager<T1> redisManager;
	private static final Cache emptyKeyFromDB;

	static {
		emptyKeyFromDB = CacheManager.create().getCache(EHCacheConstants.EMPTY_KEY_CACHE);
	}

	public CommonDaoPattern(CommonEHCacheManager<T1> cacheManager, CommonRedisManager<T1> redisManager) {
		this.cacheManager = cacheManager;
		this.redisManager = redisManager;
	}

	public CommonDaoPattern(CommonEHCacheManager<T1> cacheManager) {
		this.cacheManager = cacheManager;
	}

	public CommonDaoPattern(CommonRedisManager<T1> redisManager) {
		this.redisManager = redisManager;
	}

	public CommonDaoPattern() {
	}

	public void setCacheManager(CommonEHCacheManager<T1> cacheManager) {
		this.cacheManager = cacheManager;
	}

	protected CommonEHCacheManager<T1> getCacheManager() {
		return this.cacheManager;
	}

	public void setRedisManager(CommonRedisManager<T1> redisManager) {
		this.redisManager = redisManager;
	}

	protected CommonRedisManager<T1> getRedisManager() {
		return this.redisManager;
	}

	protected Cache getEmptyKeyFromDB() {
		return emptyKeyFromDB;
	}

	protected boolean isDBSearch() {
		boolean result = false;
		try {
			result = PropertyHandler.getBoolean("USE_LOCAL_QUERYING");
		} catch (Exception e) {
			if (LOG.isDebugEnabled())
				LOG.debug("{}", e.getStackTrace().toString());
		}
		return result;
	}

	public T1 retrieveData(String key) {
		return retrieveData(key, null);
	}

	public <T2> T1 retrieveData(String key, T2 t2) {
		T1 returnObject = null;
		try {

			SEARCH_TYPE searchType = SEARCH_TYPE.NOTHING;

			if (!commonValidation(key) || !doValidation(key, t2)) {
				if (LOG.isDebugEnabled())
					LOG.debug("validation is failed... please check >>>key:{}, DBdata:{}", key, t2);
				return null;
			}

			// 1. get from EHcache
			if (cacheManager != null) {
				searchType = SEARCH_TYPE.CACHE;
				returnObject = cacheManager.selFromCache(key);
			}

			// 2. get from REDIS
			if (redisManager != null && returnObject == null) {
				searchType = SEARCH_TYPE.REDIS;
				returnObject = redisManager.get(key);
			}

			// 3. fetch from DB
			if (returnObject == null) {
				searchType = SEARCH_TYPE.DB;
				try {
					returnObject = t2 == null ? selFromDB() : selFromDB(t2);
					if (returnObject == null)
						returnObject = t2 == null ? selFromDB(key) : selFromDB(key, t2);

					if (returnObject == null) {
						String emptyKey = _generateEmptyKey(key);
						if (emptyKey != null) {
							emptyKeyFromDB.put(new Element(emptyKey, null));
							if (LOG.isDebugEnabled())
								LOG.debug("empty key is put into EHCache>>>{}", emptyKey);
						}
					}
				} catch (Exception e) {
					LOG.error("key>>> {}, {}", key, t2, e);
				}
			}

			// 4. restore data to ehcache, Redis
			if ((searchType == SEARCH_TYPE.REDIS || searchType == SEARCH_TYPE.DB) && cacheManager != null && returnObject != null) {
				cacheManager.storeIntoCacheWithKey(key, returnObject);
			}

			if (searchType == SEARCH_TYPE.DB && redisManager != null && returnObject != null) {
				redisManager.set(key, returnObject);
			}

		} catch (Exception e) {
			e.printStackTrace();
			LOG.error(ErrorLog.getStack(e, String.format("key>>>%s, %s", key, t2)));
		}
		return returnObject;
	}

	public void loadData(String key, T1 t1) {
		try {
			// 1. load to EHcache
			if (cacheManager != null) {
				cacheManager.storeIntoCacheWithKey(key, t1);
				LOG.debug("complete load to CACHE");					
			}

			// 2. load to REDIS
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");					
			}

		} catch (Exception e) {
			LOG.error(ErrorLog.getStack(e, ""));
		}
	}

	/**
	 * DB에서 데이터를 조회한뒤 캐시와 레디스에 로딩한다.
	 *
	 * @param key
	 * @return
	 */
	public <T2> void loadDataFromDB(String key, T2 t2) {
		try {
			T1 t1 = selFromDB(t2);
			// 1. load to EHcache
			if (cacheManager != null) {
				cacheManager.storeIntoCacheWithKey(key, t1);
				LOG.debug("complete load to CACHE");
			}

			// 2. load to REDIS
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");
			}

		} catch (Exception e) {
			LOG.error("error key={}", key, e);
		}
	}

	public <T2> void loadRedisDataFromDB(String key, T2 t2) {
		try {
			T1 t1 = selFromDB(key, t2);
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");
			}
		} catch (Exception e) {
			LOG.error("error key={}", key, e);
		}
	}

	protected boolean doValidation(String key, Object t2) {
		return true;
	}

	// key 체크는 필수로 한다. empty key 가 있는지 체크한다.
	protected boolean commonValidation(String key) {
		if (StringUtils.isEmpty(key))
			return false;
		if (emptyKeyFromDB.isKeyInCache(_generateEmptyKey(key)))
			return false;
		return true;
	}

	// 디비 로직을 쓴다면 반드시 오버라이딩해서 구현.
	public T1 selFromDB() throws SQLException {
		return null;
	}

	// 디비 로직을 쓴다면 반드시 오버라이딩해서 구현.
	public T1 selFromDB(Object obj) throws SQLException {
		return null;
	}

	public T1 selFromDB(String key) throws SQLException {
		return null;
	}

	public T1 selFromDB(String key, Object obj) throws SQLException {
		return null;
	}

	protected String _generateEmptyKey(String key) {
		if (cacheManager != null) {
			return cacheManager.getCacheName() + GlobalConstants.GUBUN + key;
		} else if (redisManager != null) {
			return redisManager.generateKey(key);
		}
		return null;
	}
}

해당 클래스를 처리하기 위해 selFromDB 를 overide 받아 쿼리를 작성하는 형태로 구현하였습니다. 3Depth 기능을 스프링부트에서 구현한다고 한다면, 다음과 같이 구성할 수 있습니다.


import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class CacheConfig {

  @Bean(name = "redisCacheManager")
  public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaultConf())
            .withInitialCacheConfigurations(confMap())
            .build();
  }

  @Bean(name = "ehCacheManager")
  @Primary
  public CacheManager cacheManager() {
    return new EhCacheCacheManager(ehCacheCacheManager().getObject());
  }

  @Bean
  public EhCacheManagerFactoryBean ehCacheCacheManager() {
    EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
    cmfb.setConfigLocation(new ClassPathResource("/config/ehcache.xml"));
    cmfb.setShared(true);
    return cmfb;
  }

  private RedisCacheConfiguration defaultConf() {
    return RedisCacheConfiguration.defaultCacheConfig()
//            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
//            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofMinutes(1));
  }

  private Map<String, RedisCacheConfiguration> confMap() {
    Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
    cacheConfigurations.put("findBySspNo", defaultConf().entryTtl(Duration.ofMinutes(30L)));
    cacheConfigurations.put("findByDspList", defaultConf().entryTtl(Duration.ofMinutes(1L)));
    cacheConfigurations.put("findBycodeTpIdAndCodeId", defaultConf().entryTtl(Duration.ofMinutes(1L)));
    cacheConfigurations.put("findByGoogleSspNo", defaultConf().entryTtl(Duration.ofMinutes(30L)));
    return cacheConfigurations;
  }
}

위와 같이 redisCacheManager, ehCacheManager 을 구현한 후, @Cacheable(cacheManager = “ehCacheManager”) 를 @Cacheable(cacheManager = “redisManager”) 로 감싸주면 됩니다.