본문 바로가기

프로그래밍/Spring

Spring Framework에서의 독특한 ehcache 사용기... (1)

한동안 일이 바뻐서 블로그 관리를 못하다가 플젝 구하는 시점으로 바뀌면서 블로그를 보다보니 예전에 쓰기로 했던 Spring에서 ehcache 사용하는 내용에 대해 글을 쓰기로 했는데 쓰질 않았다. 반성하는 마음으로 지금이나마 기억을 더듬어 써보기로 한다.

 

여기 이 글에서는 Spring 에서 ehcache 설정을 하는 방법 자체를 얘기하진 않겠다. 그런 내용을 보고 싶다면 여기를 클릭해서 확인하면 된다. Spring에서 ehcache 설정에 대해 아주 잘 정리되어 있다

 

그럼 이제부터 본론으로 들어가도록 하겠다. 일반적인 Spring에서 ehcache를 사용할 경우 운영하는 사람 입장에서 받아들이기 힘든 부분이 하나 존재한다. 그것은 내가(여기서는 운영자가 될 것이다) 캐쉬되어 있는 내용을 수정할 경우 그것이 바로 캐쉬에 반영되지는 않고 정해진 시간이 지나야 캐쉬에 올라간다는 점이다(기존 설정 내용으로 이렇게 구현이 되는 방법이 있을지는 모르겠으나 적어도 내가 프로젝트를 수행하는 시점에서는 이런 내용을 발견할 수 없었다)

 

무슨 얘기인지 예를 들어보도록 하겠다. 인사 업무 관련 프로젝트에서 사용되는 인사 업무 코드를 그때그때마다 DB에서 조회할수는 없어서 Spring의 ehcache를 통해 관리되고 있다고 가정하자. 근데 누군가가 실수로 DB에 있는 코드값을 건드려 전혀 엉뚱한 코드가 등록이 되었고 이게 ehcache에 캐쉬되어 있을 경우 이를 수정하기 위해 DB를 고쳐서 올바른 값으로 변경해도 캐쉬된 값은 ehcache에 정해진 시간이 지나야 이 값이 바뀌게 된다. 대부분 이 간격을 시스템에 부하를 주지 않는 선에서 아주 짧은 시간으로 설정된다 하더라도 운영하는 사람 입장에서는 이것이 실시간으로 반영이 안되기 때문에 이 부분에 있어서 불만이 있을수 있고 이것은 내가 수행했던 프로젝트에서도 하나의 문제점으로 나온 사항이기도 했다. 즉 요약하자면 캐쉬된 값을 변경할때 그것이 실시간으로 이루어 지지 않는다는 것이다.

 

또한 값을 갱신하는 시간의 간격이 획일화되어 있는 문제가 존재한다. 우리가 캐쉬하는 데이터는 그 갱신주기가 아주 짧은 것도 있을수 있고 또 긴 것도 있을 수 있다. 근데 현재의 설정에서는 그것에 맞춰 일일이 대응을 할 수가 없다(물론 혹자는 ehcache.xml에서 각 갱신주기별로 cache 설정을 만들면 되지..하는 사항도 있을수 있으나 이 부분은 문제가 있다. 예를 들어 1분 간격으로 갱신되는 서로 다른 두가지 종류의 데이터카 캐쉬되고 있는데 그중 한가지가 업무 특성이 바뀌어서 이젠 1시간에 한번꼴로 갱신되도 된다라고 할경우 ehcache.xml에 새로운 cache 설정을 만들고 소스를 수정해야 한다. 이렇게 하는 것은 프로그래머가 아닌 운영자 입장에서는 부담스러운 작업이 된다. 

 

내가 수행했던 프로젝트에서 ehcahe에 대한 기능 요구사항은 크게 3가지로 정리가 된다.

 

1) 캐쉬의 내용이 캐쉬 설정에 정해진 캐쉬 갱신주기에 따라 갱신되는 것이 아니라 관리자가 액션을 취하면 바로 갱신이 되어야 한다

2) 캐쉬의 갱신주기를 각각 다르게 지정할수 있어야 하고 이에 대한 관리가 용이해야 한다(위의 예처럼 추가적인 ehcache 설정을 만들고 소스를 수정하는 것이 아니라 정해진 프로퍼티 파일에서 갱신주기를 관리하여 거기에서 수정하면 되는 식으로..)

3) WAS가 이중화가 되기때문에 캐시도 동기화가 이루어져야 한다

 

1), 2)번 항목은 위에서 설명했고 3)번 항목의 경우는 원래 ehcache에서 설정 작업을 해주면 동기화가 이루어 지지만 이 동기화가 내부적으로 구현되어지는 방법의 문제때문에 이 방법을 택하지 않고 다르게 구현했다. 이 내용은 기회가 닿는대로 추가로 언급하겠다

 

이로 인해 별도로 캐쉬와 관련된 공통 서비스와 그에 따른 구현체를 만들었다. 코드는 다음과 같다

 

public interface CacheService {
	
	/**
	 * 캐시에 저장할 값을 리턴하는 객체의 함수를 대신 실행하여 캐시에 고유의 캐시키를 주어 저장한다
	 * @param obj			캐시에 저장할 값을 리턴하는 함수가 있는 객체		
	 * @param methodname	캐시에 저장할 값을 리턴하는 함수의 함수명
	 * @param cachekey		캐시키
	 * @param interval		캐시 갱신 간격(0일 경우 캐시 갱신 간격을 체크하지 말고 바로 캐시에서 가져온다)
	 * @param objparam		캐시에 저장할 값을 리턴하는 함수에서 사용하는 파라미터값들
	 * @return				캐시에 저장된 내용
	 * @throws Exception
	 */
	public Object getCache(Object obj, String methodname, String cachekey, long interval, Object ... objparam) throws Exception;
	
	/**
	 * 캐시키를 주어 해당 캐시키에 저장되어 있는 캐시내용을 리턴한다
	 * @param cachekey		캐시키
	 * @param objparam		캐시키를 만들때 사용하는 파라미터 값들
	 * @return				캐시에 저장된 내용
	 * @throws Exception
	 */
	public Object getCache(String cachekey, Object ... objparam) throws Exception;
	
	/**
	 * 현재 사용중인 캐쉬의 크기를 리턴한다
	 * @return				사용중인 캐쉬의 크기
	 * @throws Exception
	 */
	public long getCacheSize() throws Exception;
}

 

위의 인터페이스를 구현한 구현체 소스는 다음과 같다

 

public class CacheServiceImpl implements CacheService {

	private final Log logger = LogFactory.getLog(this.getClass());
	
	@Autowired
	CacheManager cachemanager;
	
	@Override
	public Object getCache(Object obj, String methodname, String cachekey, long interval, Object... objparam) throws Exception{
		// TODO Auto-generated method stub
		Object result = null;
		Cache cache = cachemanager.getCache("selectCache");
		StringBuffer sbCachekey = new StringBuffer(cachekey);
		
		// 고유한 캐쉬키를 만들기 위해 그 당시 사용했던 파라미터값을 같이 조합하여 캐쉬키를 유니크하게 만든다(파라미터 값을 -(하이픈)으로 연결하여 캐쉬키를 만든다)
		if((objparam != null) && (objparam.length > 0)){
			for(Object param : objparam){
				sbCachekey.append("-");
				sbCachekey.append(param);
			}
		}
		
		String useCachekey = sbCachekey.toString();
		logger.debug("사용 캐쉬키  : " + useCachekey);
		
		Element element = cache.get(useCachekey);
		
		if(element == null){
			logger.debug("캐쉬에 없어서 새로 넣는다");
			logger.debug("생성때 currentTimeMillis : " + System.currentTimeMillis());
			result = getData(obj, methodname, objparam);
			cache.put(new Element(useCachekey, result));
		}else{
			logger.debug("캐쉬꺼 꺼내오기");
			logger.debug("getCreationTime : " + element.getCreationTime());
			logger.debug("검색때 currentTimeMillis : " + System.currentTimeMillis());
			
			if(interval == 0){				// interval이 0일때는 갱신간격 체크를 하지 말고 바로 캐쉬에서 가져와서 리턴한다
				result = element.getObjectValue();
			}else if(interval == -1){		// interval이 -1일때는 무조건 조회한뒤에 캐쉬에 넣고 리턴
				try{
					result = getData(obj, methodname, objparam);
					cache.put(new Element(useCachekey, result));
				}catch(Exception e){
					result = element.getObjectValue();
				}
			}else{							// interval이 0이 아닐때는 갱신간격 체크를 해서 DB에서 조회해서 캐시에 넣거나 또는 바로 캐쉬에서 가져와서 리턴한다
				long timeoutgap = System.currentTimeMillis() - element.getCreationTime();
				logger.debug("timeoutgap : " + timeoutgap);
				if(timeoutgap >= interval){
					try{
						result = getData(obj, methodname, objparam);
						cache.put(new Element(useCachekey, result));
					}catch(Exception e){
						result = element.getObjectValue();
					}
				}else{
					result = element.getObjectValue();
				}
			}
			
		}
		
		return result;
	}
	
	@Override
	public Object getCache(String cachekey, Object... objparam)
			throws Exception {
		// TODO Auto-generated method stub
		Object result = null;
		Cache cache = cachemanager.getCache("selectCache");
		StringBuffer sbCachekey = new StringBuffer(cachekey);
		
		// 고유한 캐쉬키를 만들기 위해 그 당시 사용했던 파라미터값을 같이 조합하여 캐쉬키를 유니크하게 만든다(파라미터 값을 -(하이픈)으로 연결하여 캐쉬키를 만든다)
		if((objparam != null) && (objparam.length > 0)){
			for(Object param : objparam){
				sbCachekey.append("-");
				sbCachekey.append(param);
			}
		}
		
		String useCachekey = sbCachekey.toString();
		
		Element element = cache.get(useCachekey);
		
		// 캐쉬에 저장되어 있을 경우
		if(element != null){
			result = element.getObjectValue();
		}
		
		return result;
		
	}



	private Object getData(Object obj, String methodname, Object... objparam) throws Exception{
		
		Class clazz = obj.getClass();
		Class [] args = new Class[objparam.length];
		for(int i=0; i < args.length; i++){
			
			logger.debug("Class name : " + objparam[i].getClass().getName());
			
			/*
			 * Java Reflection에서 method에 파라미터 사용시 해당 파라미터 타입이 int(primitive 타입)를 사용하든 Integer를 사용하든 똑같이 Integer 클래스로 인식한다
			 * 때문에 캐쉬를 사용하는 함수의 파라미터는 primitive 함수로 설계해야 한다
			 */
			if(objparam[i] instanceof String){
				args[i] = String.class;
			}else if(objparam[i] instanceof Integer){
				args[i] = int.class;
			}else if(objparam[i] instanceof Long){
				args[i] = long.class;
			}else if(objparam[i] instanceof Float){
				args[i] = float.class;
			}else if(objparam[i] instanceof Double){
				args[i] = double.class;
			}else if(objparam[i] instanceof Boolean){
				args[i] = boolean.class;
			}else if(objparam[i] instanceof Byte){
				args[i] = byte.class;
			}else if(objparam[i] instanceof Character){
				args[i] = char.class;
			}else{
				args[i] = objparam[i].getClass();
			}
			
			
		}
		
		Method method = clazz.getMethod(methodname, args);
		Object result = method.invoke(obj, objparam);
		return result;
	}

	@Override
	public long getCacheSize() throws Exception {
		// TODO Auto-generated method stub
		// 캐시 크기가 이상하다고 느껴질경우엔 캐시에 저장되는 클래스들이 Serializable 인터페이스를 구현했는지 확인한다
		Cache cache = cachemanager.getCache("selectCache");
		return cache.calculateInMemorySize();
	}
	
}

 

다음에는 이 소스에 대한 내용을 가지고 구체적으로 설명하도록 하겠다