memcached는
memcached - http://danga.com/memcached/
memcached client for Java - http://www.whalin.com/memcached/
<bean id="memCachedClient"위 내용은 memcached client for Java의 예제 코드 설정 내용을 그대로 적용한 것이다. 각 항목에 대한 자세한 내용은 memcached client for Java를 참조하면 된다. SockIOPool은 싱글턴 인스턴스를 얻기 위한 factory 메소드인 getInstance(), 초기화 메소드인 initialize(), 리소스 해제를 위한 메소드인 shutDown()을 제공하고 있으므로 Spring bean으로 등록할 때 이 내용을 적어주도록 하자.
class="com.danga.MemCached.MemCachedClient"
depends-on="sockIOPool">
<property name="compressEnable" value="true" />
<property name="compressThreshold" value="65536" /><!-- 64K -->
</bean>
<bean id="sockIOPool" class="com.danga.MemCached.SockIOPool"
factory-method="getInstance" init-method="initialize"
destroy-method="shutDown">
<property name="servers">
<list>
<value>localhost:11211</value>
</list>
</property>
<property name="weights">
<list>
<value>1</value>
</list>
</property>
<property name="initConn" value="5" />
<property name="minConn" value="5" />
<property name="maxConn" value="10" />
<property name="maxIdle" value="21600000" /><!-- 6 hours: 1000 * 60 * 60 * 6 -->
<property name="maintSleep" value="30" />
<property name="nagle" value="false" />
<property name="socketTO" value="3000" />
<property name="socketConnectTO" value="0" />
</bean>
package justfun.spring.aspect;Asepct 내용은 매우 단순하다. 먼저 메소드 호출에 대한 Unique한 키를 생성하고 캐시에서 데이터를 찾은 후 있으면 그것을 리턴한다. 캐시에서 데이터가 없으면 target 메소드를 실행(joinPoint.proceed())한 후 결과를 캐시에 저장하고 리턴한다.
import java.util.Date;
import java.util.Map;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import com.danga.MemCached.MemCachedClient;
public class MemCached {
private static Logger LOG = Logger.getLogger(MemCached.class);
private MemCachedClient mcc;
public void setMemCachedClient(MemCachedClient mcc) {
this.mcc = mcc;
}
public Object methodCache(ProceedingJoinPoint joinPoint) throws Throwable {
//printStats();
LOG.debug("-----------------------------------------------");
StringBuffer key = new StringBuffer();
key.append(joinPoint.getTarget().getClass().getName());
key.append("$").append(joinPoint.toShortString());
key.append("[");
Object[] args = joinPoint.getArgs();
if (args != null) {
for (Object arg : args) {
if (arg != null)
key.append(arg);
}
}
key.append("]");
LOG.debug(key.toString());
Object o = null;
if (mcc.keyExists(key.toString())) {
Date s = new Date();
o = mcc.get(key.toString());
Date e = new Date();
long ret = e.getTime() - s.getTime();
LOG.debug("cache hit [" + ret + "ms]");
}
else {
Date s = new Date();
o = joinPoint.proceed();
if (o != null)
mcc.add(key.toString(), o);
Date e = new Date();
long ret = e.getTime() - s.getTime();
LOG.debug("method execution [" + ret + "ms]");
}
LOG.debug("-----------------------------------------------");
return o;
}
@SuppressWarnings("unchecked")
public void printStats() {
Map stats = mcc.stats();
System.out.println("[STATS]");
System.out.println(stats);
System.out.println();
}
}
<bean id="memcached" class="justfun.spring.aspect.MemCached">위 설정은 memcached Aspect를 justfun.spring.domain.service 패키지에 존재하는 모든 클래스의 모든 메소드에 적용하는 설정이다. AOP 표현식에 대한 내용은 Spring AOP의 내용을 참조하면 된다.
<property name="memCachedClient" ref="memCachedClient" />
</bean>
<aop:config>
<aop:aspect id="memcachedAspect" ref="memcached">
<aop:pointcut id="service"
expression="execution(* justfun.spring.domain.service..*.*(..))" />
<aop:around pointcut-ref="service" method="methodCache" />
</aop:aspect>
</aop:config>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
package justfun.ibatis;iBatis의 CacheController 인터페이스는 5개의 메소드를 구현하도록 되어 있으며 이 메소드들을 통해 memcached를 사용하도록 MemCachedController를 구현하였다.
import java.util.Properties;
import org.apache.log4j.Logger;
import com.danga.MemCached.MemCachedClient;
import com.ibatis.sqlmap.engine.cache.CacheController;
import com.ibatis.sqlmap.engine.cache.CacheModel;
public class MemCachedController implements CacheController {
private static Logger LOG = Logger.getLogger(MemCachedController.class);
private MemCachedClient mcc;
public MemCachedController() {
mcc = new MemCachedClient();
}
@Override
public void flush(CacheModel cacheModel) {
LOG.debug("flush");
mcc.flushAll();
}
@Override
public Object getObject(CacheModel cacheModel, Object key) {
Object o = mcc.get(key.toString());
LOG.debug("getObject: " + key);
LOG.debug("\t" + o);
return o;
}
@Override
public void putObject(CacheModel cacheModel, Object key, Object object) {
LOG.debug("putObject: " + key);
mcc.add(key.toString(), object);
}
@Override
public Object removeObject(CacheModel cacheModel, Object key) {
LOG.debug("removeObject: " + key);
Object o = mcc.get(key.toString());
mcc.delete(key.toString());
return o;
}
@Override
public void setProperties(Properties props) {
}
}
<cacheModel id="memcached-cache" type="justfun.ibatis.MemCachedController">이제 iBatis에서 제공하는 flush 옵션들을 통해 캐시를 쉽게 업데이트 할 수 있게 되었다. 위 설정에서는 insert, update, delete 라는 id로 정의된 statememt가 수행될 때 캐시의 flush를 수행하도록 되어 있다. 언제 flush를 할 것인가는 각자의 요구사항에 맞게 설정하면 된다.
<flushInterval hours="24"/>
<flushOnExecute statement="insert"/>
<flushOnExecute statement="update"/>
<flushOnExecute statement="delete"/>
</cacheModel>
캐시 만료(Expire)를 고려한 MemCachedAspect 작성하기
앞서 memcached를 사용하여 메소드 호출 시 캐시를 사용하도록 하는 Aspect를 작성했었다. 이전에 구현한 Aspect는 데이터에 변동이 있을 때 캐시를 지우지 못하는 문제가 있었는데, 이러한 이유(캐시 만료)로 iBatis의 CacheController를 구현한 클래스를 작성했으나 이 클래스는 iBatis에 의존적이므로 iBatis를 사용하지 않는 어플리케이션에서는 적용할 수 없다. 따라서 여기서는 캐시의 만료 정책을 고려한 보다 향상된 Aspect를 작성하고자 한다.
(구현 과정에서 클래스 및 몇몇 메소드 명이 변경되었으니 이 점 유념하시길....)
기본적인 캐시 만료 정책은 매우 간단하다. 자바의 정규식으로 표현된 특정 패턴의 캐시 key와 그 키에 해당될 때 만료시켜야 할 캐시 key의 패턴을 정의하여 이를 이용해 캐시 데이터를 만료시키는 것이다. 이를 위해서는 자바 정규식을 입력받아 관리할 수 있어야 하며, 캐시에 저장된 key들의 목록을 관리할 수 있어야 한다.
여기서도 자질구레한 설명은 필요 없다. 바로 코드를 보자.
package justfun.spring.aspect;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import com.danga.MemCached.MemCachedClient;
/**
*
* @author jhcho
*
*/
public class MemCachedAspect {
private static Logger LOG = Logger.getLogger(MemCachedAspect.class);
private MemCachedClient mcc;
private ExpireCond[] expireCond;
private CacheKeyPool keyPool;
public MemCachedAspect() {
keyPool = CacheKeyPool.getInstance();
}
public void setMemCachedClient(MemCachedClient mcc) {
this.mcc = mcc;
}
public void setExpireCond(ExpireCond[] expireCond) {
this.expireCond = expireCond;
}
@SuppressWarnings("unchecked")
private String generateKey(JoinPoint joinPoint) {
Class targetClass = joinPoint.getTarget().getClass();
Signature targetMethod = joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
StringBuffer key = new StringBuffer();
key.append(targetClass.getName());
key.append(".").append(targetMethod.getName());
key.append("(");
if (args != null) {
int argCount = args.length;
for (Object arg : args) {
if (arg != null)
key.append(arg);
else
key.append("null");
if (arg != args[argCount - 1])
key.append(",");
}
}
key.append(")");
return key.toString();
}
private void doFlush(String key) {
if (expireCond == null)
return;
LOG.debug("CHECK EXPIRE ");
Pattern targetPattern, flushPattern;
Matcher targetMatcher, flushMatcher;
String[] keys = keyPool.list();
List<String> flushKeyList = new ArrayList<String>();
for (ExpireCond cond : expireCond) {
targetPattern = Pattern.compile(cond.getTargetKey());
flushPattern = Pattern.compile(cond.getExpireKey());
targetMatcher = targetPattern.matcher(key); // for target key searching
if (targetMatcher.find()) {
for (String cacheKey : keys) {
flushMatcher = flushPattern.matcher(cacheKey); // for flush key searching
if (flushMatcher.find()) {
flushKeyList.add(cacheKey);
}
}
}
}
if (flushKeyList.size() > 0) {
for (String flushKey : flushKeyList) {
LOG.debug("DO EXPIRE");
if (mcc.delete(flushKey)) {
keyPool.remove(flushKey);
LOG.debug("\tKey: " + flushKey + " [SUCCESS]");
}
else {
LOG.debug("\tKey: " + flushKey + " [FAIL]");
}
}
}
}
/**
* Aspect method. invoke and cache a result object of target method.
*
* @param joinPoint
* @return a result of target method
* @throws Throwable
*/
@SuppressWarnings("unchecked")
public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable {
String key = generateKey(joinPoint);
// flush some caches
doFlush(key);
// printStats();
LOG.debug("-----------------------------------------------");
LOG.debug("key: " + key);
Object o = null;
if (mcc.keyExists(key)) {
if (!keyPool.contains(key))
keyPool.add(key);
Date s = new Date();
o = mcc.get(key);
Date e = new Date();
long ret = e.getTime() - s.getTime();
LOG.debug("cache hit [" + ret + "ms]");
}
else {
Date s = new Date();
o = joinPoint.proceed();
if (o != null) {
if (mcc.add(key, o))
keyPool.add(key);
}
Date e = new Date();
long ret = e.getTime() - s.getTime();
LOG.debug("method execution [" + ret + "ms]");
}
LOG.debug("-----------------------------------------------");
return o;
}
@SuppressWarnings("unchecked")
public void printStats() {
Map stats = mcc.stats();
System.out.println("[STATS]");
System.out.println(stats);
System.out.println();
}
}
※ 클래스명을 MemCached에서 MemCachedAspect로 변경하였고, 메소드 명을 methodCache에서 invoke로 변경하였다.
위 코드에서 눈여겨 볼 부분은 invoke 시 먼저 doFlush 메소드를 호출하여 캐시 만료 조건을 검사하도록 하는 것이다. doFlush는 캐시 만료 조건인 ExpireCond[]를 순차적으로 탐색하며 현재 메소드에 해당하는 키와 일치하는지 검사한 후 이와 일치하면 현재 캐시에 저장되어 있는 Key의 목록을 순차적으로 탐색하며 만료시킬 대상을 찾는다.
만약 만료 대상이 존재하면 해당 캐시를 지운다.
이 때 정규표현식은 자바의 정규표현식을 따르며 패턴 매칭은 검사는 java.util.regex.Pattern과 java.util.regex.Matcher를 사용하였다. 그리고 패턴을 검사할 때 find() 메소드를 사용하기 때문에 정확하게 일치하는 패턴을 작성하지 않아도 된다.
MemCachedAspect는 ExpireCond[] 와 CacheKeyPool 을 갖는다. ExpireCond는 캐시 만료 조건(패턴)을 저장하고 있는 빈이고 CacheKeyPool은 현재 캐시에 저장된 데이터들의 캐시 Key 목록을 관리하기 위한 클래스이다.
☞ ExpireCond.java
package justfun.spring.aspect;☞ CacheKeyPool.java
/**
* Expire condition.
*
* @author jhcho
*
*/
public class ExpireCond {
private String targetKey;
private String flushKey;
public String getTargetKey() {
return targetKey;
}
public void setTargetKey(String targetKey) {
this.targetKey = targetKey;
}
public String getExpireKey() {
return expireKey;
}
public void setExpireKey(String expireKey) {
this.expireKey = expireKey;
}
}
package justfun.spring.aspect;
import java.util.List;
import java.util.Vector;
public class CacheKeyPool {
private List<String> pool;
private CacheKeyPool() {
pool = new Vector<String>();
}
public static CacheKeyPool getInstance() {
return Holder.instance;
}
public boolean contains(String key) {
return pool.contains(key);
}
public void add(String key) {
pool.add(key);
}
public void remove(String key) {
pool.remove(key);
}
public void remove(String[] keys) {
for (String key : keys) {
remove(key);
}
}
public String[] list() {
return pool.toArray(new String[0]);
}
private static class Holder {
static final CacheKeyPool instance = new CacheKeyPool();
}
}
<bean id="memcached" class="justfun.spring.aspect.MemCachedAspect">
<property name="memCachedClient" ref="memCachedClient" />
<property name="expireCond">
<list>
<bean class="justfun.spring.aspect.ExpireCond">
<!-- if "write", "modify", "delete" method invoked, delete cache data of "list" method -->
<property name="targetKey"
value="MemoServiceImpl\.(write|modify|delete)" />
<property name="expireKey"
value="MemoServiceImpl\.(list)" />
</bean>
</list>
</property>
</bean>
<aop:pointcut id="service"
expression="execution(* net.daum.spring.domain.service..*.*(..))" />
<aop:around pointcut-ref="service" method="invoke" />
출처: http://tinywolf.tistory.com/80, http://tinywolf.tistory.com/81
Ambiguous mapping found. Cannot map '**Controller' bean method (0) | 2016.03.28 |
---|---|
spring-boot (0) | 2016.03.11 |
ibatis 객체 안에 리스트객체 또 그 안에 리스트 객체 사용법 (0) | 2015.09.03 |
ibatis 달러문자($) 입력 오류 (0) | 2015.09.01 |
referer (레퍼러) (0) | 2015.07.14 |
댓글 영역