2013년 5월 27일 월요일

Time Period Library for JVM

기업용 솔루션을 만드는 경우 시간에 대한 계산이 참 많습니다.
특히, 일반적인 산술적 계산이 아니라 주말, 공휴일, 개인 휴가 (반차 포함), 팀 공가 등등을 고려한 작업 계획을 작성하려면 좀 복잡한 계산이 필요합니다.

이런 걸 일반적으로 Company Working Calendar 라고 하는데, 기간별 투입시간 등의 리소스 관리에 많은 기법이 동원됩니다.

예전 회사에서도 이 문제로 여러번에 걸쳐 발전시켰습니다만, 워낙 좋은 라이브러리가 있어서 이 넘의 forking 해서 사용했습니다.

원본 : Time Period Library for .NET

원본을 보면, 제가 원하는 기간 계산이나 최종일 계산등이 아주 구조적으로 할 수 있도록 구성되어 있습니다.

제가 여기에 몇가지 기능을 추가해서 사용했었는데요. .NET의 TPL 을 이용한 병렬 처리가 가장 기억에 남습니다.

올 1월에 scala 공부 겸 해서 scala 로 제작해 보자 해서 porting 을 수행하였습니다.

1. Time Period Library by Scala

뭐 Scala 로 제작했으니 JVM에서 돌구요. Scala의 Collection 지원과 여러가지 기능 (curry 등) 을 사용하였으니, scala 공부 시에는 도움이 될 듯 합니다.

문제는 제가 이넘 제작하다가 테스트 코드를 많이 못 만들어 버그가 많을 것이라는 것입니다^^

다음은 java 로 porting 한 소스입니다.

2. Time Period Library by Java

이넘은 debop4j 라이브러리 내에서 개발한 거라 debop4j-core 를 사용합니다.
기능은 위의 두 개와 같구요. java 코드라 좀 지저분하다는 점이 단점이지요^^

이 글을 빌어 원작자에게 감사드리며,  허락도 받지 않은 상태라 걱정도 되네요.

2013년 5월 22일 수요일

hibernate-ogm 을 이용한 검색서비스 개발

올 3월 갑작스레 맡게 된 검색 서비스 개발 업무를 어떻게 할까 고민하다가 그래도 내가 잘하는 방법대로 얼른 만들어보자^^ 하면서 검색 관련 정보를 수집했습니다.

1. hibernate-search + lucene 를 사용한다.
2. 한글형태소 분석기가 있어야 한다.
3. 한글형태소 분석기의 품질은 사전이 정의한다.

라는 몇가지 룰과 정보를 얻었습니다...
검색 품질을 생각하지 않는다면 n-gram 을 사용하는 CJKAnalyzer 를 사용해도 됩니다만... 이 놈은 마지막 쵸이스로 놓고, 대강 hibernate-search 와 lucene 을 이용하여 개발했습니다...

몇가지 새로운 사실도 알아내고, 성능을 높힐 수 있었지만, 좀 더 욕심을 내서 다음과 같은 작업을 더 수행했습니다.
hibernate-search가 Index 정보를 sharding 할 수도 있더군요. 비동기 방식으로 index 작업할 수도 있구요.

1. hibernate-ogm 을 사용해보자. - MongoDB를 저장소로 사용한다.
2. 한글형태소 분석기를 써보자 (이수명씨 것을 기본으로 몇가지 수정 및 추가)

위 두 가지를 성공적으로 적용했습니다.

간단하게나마 발표자료로 만들었습니다.




다음에는 hibernate-ogm 자체에 대한 글을 써 볼까 합니다.

2013년 5월 19일 일요일

PostgreSQL 관련 추천 자료

PostgreSQL 을 공부하다보니, 한글 자료 및 국내 사례는 거의 없네요^^ 그래도 미국, 유럽, 일본에서는 상당히 많은 적용사례가 있고,  제게는 기능 상 MySQL 보다 더 매력적으로 다가오네요.

몇가지 PostgreSQL 관련 자료를 정리해 봅니다.


  1. 한눈에 살펴보는 PostgreSQL
  2. PostgreSQL 용 유용한 Extensions 
  3. pgpool-ii tutorial ( connection pool, load balancing, replication 제공)
  4. hstore 란?  
  5. hstore for java, java-hstore-sample ( key-value for PostgreSQL)
  6. pgmemcache Setup and Usage (memcache 를 이용한 캐시)
  7. PostreSQL 9.0 streaming replication + pgpool-ii


특히 제게는 PostgreSQL 자체의 많은 기능도 좋지만, pgpool-ii 같이 좋은 load balancing 이 있다는 것과 엄청난 extensions 들이 굳이 상용을 쓸 필요 있을까 싶네요^^

2013년 5월 7일 화요일

Spring framework 에서 static field에 injection 하기

Spring 전문가라면 모두 아시리라 믿습니다만, 전 .NET 에서 Castle.Windsor 를 주로 이용하여 Spring 이 좀 낫섭니다. 특히 static field 나 class 에 대한 정보가 별로 없더군요.

물론 제가 아직 Spring에 대한 내공이 부족하기도 하고, .NET의 Extension Methods 를 자주 사용하다보니 static class 를 습관적으로 많이 써서 생기는 문제일 수 있습니다. (이거 좋은 습관은 아닌데...) 
어쨌든 기존 소스 중에 Singleton으로 써야 하고, static field 를 가진 클래스에 대해 injection을 수행해야 하는 것이 있었습니다.  다음과 같은 Class 가 있었습니다.

UnitOfWorks.java

package example.spring.staticinjection;
public class UnitOfWorks {
private static UnitOfWorkFactory unitOfWorkFactory;
public static void setUnitOfWorkFactory(UnitOfWorkFactory factory) {
unitOfWorkFactory = factory;
}
public static UnitOfWorkFactory getUnitOfWorkFactory() {
return unitOfWorkFactory;
}
}


UnitOfWorks 는 static field로 UnitOfWorkFactory를 가지고 있습니다. 이 필드를 Spring 이 injection을 할 수 있도록 해주고 싶었습니다.

xml 에서 작업하려면 MethodInvokingFactoryBean 을 이용하여 static method 를 호출하면 됩니다.

application-context.xml

<!-- UnitOfWorkFactory 인스턴스를 UnitOfWorks Singleton의 static field에 설정합니다. -->
<bean name="unitOfWorkInitializer" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="kr.nsoft.data.hibernate.unitofwork.UnitOfWorks.setUnitOfWorkFactory"/>
<property name="arguments">
<list>
<ref bean="unitOfWorkFactory"/>
</list>
</property>
</bean>

xml 에서도 되는데 java configuration에서도 되겠지요?
그래서 구글링을 했더니 다행히, 몇번만에 해답을 찾았습니다.
원문: Spring annotations static injection tutorial
이 문서를 보고, 핵심은 원하는 클래스를 @Component로 지정하여 Spring Container에서 관리하도록하고, injection을 수행하도록 @Autowired를 지정하면 됩니다.

새로운 UnitOfWorks.java

package example.spring.staticinjection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UnitOfWorks {
private static UnitOfWorkFactory unitOfWorkFactory;
/**
* Spring가 UnitOfFactory 를 인젝션을 수행합니다.
*
* @param factory
*/
@Autowired
public void setUnitOfWorkFactory(UnitOfWorkFactory factory) {
unitOfWorkFactory = factory;
}
public static UnitOfWorkFactory getUnitOfWorkFactory() {
return unitOfWorkFactory;
}
}

UnitOfWorks 클래스는 @Component로 지정하고, @Autowired 해야 할 메소드는 static 이 아닌 instance method로 변경한 것으로 끝났다^^

마지막으로 Spring java configuration 에서 위의 UnitOfWorks를 ComponentScan 으로 등록해주면 됩니다.

StaticInjectionConfig.java

package example.spring.staticinjection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan( basePackageClasses = { UnitOfWorks.class } )
public class StaticInjectionConfig {
@Bean
public UnitOfWorkFactory unitOfWorkFactory() {
return new UnitOfWorkFactory();
}
}


ComponentScan에서 UnitOfWorks class 를 지정해서 Component로 등록하게 하면, Bean으로 등록된 UnitOfWorkFactory가 injection이 됩니다^^
이로서 static field에 대해서도 java configuration에서 설정할 수 있게 되었습니다.
흠... Spring 에 대해 좀 더 공부해야겠네요...

2013년 5월 3일 금요일

Hibernate Criteria 의 장점

ORM의 대표격인 Hibernate 는 여러가지 장점 중에서도 질의어에 대한 지원 방식 풍부하다는 점이다.


  1. Criteria
    • 프로그램 언어로 빌더 패턴을 차용한 방식으로 질의를 빌드합니다.
    • 사용자 정의의 Criteria 를 정의하여 사용할 수 있습니다.
    • Criteria 를 실제 SQL 문으로 변환하는데 비용이 드는 단점이 있습니다.
  2. HQL (Hibernate Query Language)
    • hibernate 고유의 질의어입니다. 
    • SQL 표준 문법과 유사하지만 새로 배워야 합니다.
    • 변환에 드는 비용이 한번만 있고, 캐시하여 사용하므로 성능 상 유리합니다.
  3. SQL String
    • 기존 표준 문법이므로 장단점이 따로 없습니다.
    • ORM 에서도 그냥 사용해도 된다는 점

이 있다.

SQL 에 익숙한 개발자들은 Criteria 의 가능성 및 효용성에 대해 느껴보지 못하고, 과소평가하는 경우가 많은데, 이 때 제가 아주 간단한 과제를 통해, Criteria 의 장점을 체험하게 합니다.

간단한 과제:  작업 기간에 대해 검색 조건으로 시작시각과 완료시각을 입력 받아 겹치는 기간에 있는 작업 정보를 조회하는 SQL 쿼리를 작성해 보시오.

위의 과제를 주면 99.9% 다 틀립니다. 0.1% 해결한다 하더라도, 너무 복잡해 다른 사람들은 사용하기에도 힘들 것입니다.

위 문제의 첫번째 난관은 기간에 해당하는  시작시각과 완료시각이 nullable 임을 인지하는 것입니다. 두 번째는 검색하고자하는 시작시각과 완료시각도 nullable 일 수 있다는 것입니다. 그럼 이런 조건 하에서 검색을 위한 질의어를 만드는데 경우의 수는 몇이나 될까요?
간단하게 2*2*2*2 = 2^4 이죠?
뭐 중딩 1차원 그래프까지 그려가면서 설명할 필요 없겠죠?
이걸 SQL 쿼리문으로 나타내라구요?

뭐 할 수는 있겠지요... 다만, 차라리 Criteria 라는 좋은 방식이 있으니 그걸 이용해 보라는 겁니다.

결론부터 말하면 다음과 같이 구현하면 됩니다.
OverlapCriteria

/** 지정한 범위 값이 두 속성 값 구간과 겹치는지를 알아보기 위한 질의어 */
public static Criterion getIsOverlapCriterion(String loPropertyName,
String hiPropertyName,
Object lo,
Object hi,
boolean includeLo,
boolean includeHi) {
if (lo == null && hi == null)
throw new IllegalArgumentException("lo, hi 모두 null 값이면 질의어를 만들 수 없습니다.");
if (log.isDebugEnabled())
log.debug(String.format("build getIsOverlapCriterion... loPropertyName=[%s], hiPropertyName=[%s]" +
" lo=[%s], hi=[%s], includeLo=[%s], includeHi=[%s]",
loPropertyName, hiPropertyName, lo, hi, includeLo, includeHi));
if (lo != null && hi != null) {
return Restrictions
.disjunction()
.add(getIsInRangeCriterion(loPropertyName, hiPropertyName, lo, includeLo, includeHi))
.add(getIsInRangeCriterion(loPropertyName, hiPropertyName, hi, includeLo, includeHi))
.add(getIsBetweenCriterion(loPropertyName, lo, hi, includeLo, includeHi))
.add(getIsBetweenCriterion(hiPropertyName, lo, hi, includeLo, includeHi));
}
if (lo != null) {
return Restrictions
.disjunction()
.add(getIsInRangeCriterion(loPropertyName, hiPropertyName, lo, includeLo, includeHi))
.add((includeLo) ? ge(loPropertyName, lo)
: gt(loPropertyName, lo))
.add((includeLo) ? ge(hiPropertyName, lo)
: gt(hiPropertyName, lo));
} else {
return Restrictions
.disjunction()
.add(getIsInRangeCriterion(loPropertyName, hiPropertyName, hi, includeLo, includeHi))
.add((includeLo) ? le(loPropertyName, hi)
: lt(loPropertyName, hi))
.add((includeLo) ? le(hiPropertyName, hi)
: lt(hiPropertyName, hi));
}
}
뭐 null 값인지 검사해서 알맞은 메소드를 호출하면 되네요^^
헐... 근데 내부에 보니 Between 과 InRange 라는 연산자를 사용하네요... 이건 뭐죠?

Between (일반적인 쿼리문에서는 A <= X <= B 이다. 경계값을 포함하는 closed 이다)

public static Criterion getIsBetweenCriterion(String propertyName, Object lo, Object hi,
boolean includeLo, boolean includeHi) {
shouldNotBeEmpty(propertyName, "propertyName");
if (lo == null && hi == null)
throw new IllegalArgumentException("상하한 값 모두 null 이면 안됩니다.");
if (lo != null && hi != null && includeLo && includeHi)
return between(propertyName, lo, hi);
// lo, hi 값 중 하나가 없다면
Conjunction result = conjunction();
if (lo != null)
result.add((includeLo) ? ge(propertyName, lo)
: gt(propertyName, lo));
if (hi != null)
result.add((includeHi) ? le(propertyName, hi)
: lt(propertyName, hi));
return result;
}
InRange 는 Between 과 대상과 검사 값이 반대일 뿐이다.

/** 지정한 값이 두 속성 값 사이에 존재하는지 여부 */
public static Criterion getIsInRangeCriterion(final String loPropertyName,
final String hiPropertyName,
Object value,
boolean includeLo,
boolean includeHi) {
Guard.shouldNotBeNull(value, "value");
Criterion loCriteria = (includeLo) ? le(loPropertyName, value)
: lt(loPropertyName, value);
Criterion hiCritiera = (includeHi) ? ge(hiPropertyName, value)
: gt(hiPropertyName, value);
return conjunction()
.add(disjunction()
.add(isNull(loPropertyName))
.add(loCriteria))
.add(disjunction()
.add(isNull(hiPropertyName))
.add(hiCritiera));
}

자 이제 Criteria 가 SQL 문보다 논리적으로 쉽게 구현되는지 알겠는가?
SQL문을 먼저 배웠다고, 좀 더 익숙하다고 그게 진리는 아닙니다.
java가 되었건, csharp 이 되었건, scala 가 되었건 익숙한 것보다 유용하게 표현할 수 있는 방식이 있다면 그게 진리입니다.


2013년 5월 2일 목요일

redis 를 hibernate 2nd cache 로 사용하기 - part 2


hibernate-redis 제작 - part 1
hibernate-redis 제작 - part 2


redis 가 캐시 시스템으로 사용하기 좋다는 것은 여러 사례에서 알 수 있고, SNS 같은 경우에는 Timeline 정보를 캐싱하는데 자주 사용하지요.

이런 redis 를 hibernate 의 2nd cache  저장소로 사용하면 좋겠다고 생각해서, hibernate-redis 를 제작했습니다.

첫번째 버전은 제가 Redis에 대해 잘 몰라, 그냥 Spring Data Redis 를 사용하여, 구현했습니다.
개발하다보니 Jedis 만을 사용하여 개발하는 것이 더 가볍게 개발 할 수 있을 것 같아, Spring Data Redis 를 걷어내고, Jedis 만을 사용하여 개발했습니다.

hibernate-redis 는 현재 0.9.0 이고, 단위 테스트를 통과한 상태입니다.
아직 실전 테스트가 남아 있습니다^^

이번 글에서는 일반적인 캐시로서 Redis를 이용할 때, 직접 jedis 를 사용할 수도 있지만,  좀 더 쉽게 만든 JedisClient 에 대해 소개하기로 하겠습니다.

JedisClient.java

/*
* Copyright 2011-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.cache.redis.jedis;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.redis.serializer.BinaryRedisSerializer;
import org.hibernate.cache.redis.serializer.RedisSerializer;
import org.hibernate.cache.redis.serializer.SerializationTool;
import org.hibernate.cache.redis.util.CollectionUtil;
import org.hibernate.cache.spi.CacheKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* RedisClient implements using Jedis library
* <p/>
* 참고 : https://github.com/xetorthio/jedis/wiki/AdvancedUsage
*
* @author 배성혁 ( sunghyouk.bae@gmail.com )
* @since 13. 4. 9 오후 10:20
*/
public class JedisClient {
private static final Logger log = LoggerFactory.getLogger(JedisClient.class);
private static final boolean isTraceEnabled = log.isTraceEnabled();
private static final boolean isDebugEnabled = log.isDebugEnabled();
public static final int DEFAULT_EXPIRY_IN_SECONDS = 120;
public static final String DEFAULT_REGION_NAME = "hibernate";
@Getter
private final String regionName;
@Getter
private final JedisPool jedisPool;
@Getter
@Setter
private int database;
@Getter
@Setter
private int expiryInSeconds;
@Getter
@Setter
private RedisSerializer keySerializer = new BinaryRedisSerializer<Object>();
@Getter
@Setter
private RedisSerializer valueSerializer = new BinaryRedisSerializer<Object>();
public JedisClient() {
this(DEFAULT_REGION_NAME, new JedisPool("localhost"));
}
public JedisClient(JedisPool jedisPool) {
this(DEFAULT_REGION_NAME, jedisPool);
}
public JedisClient(String regionName, JedisPool jedisPool) {
this(regionName, jedisPool, DEFAULT_EXPIRY_IN_SECONDS);
}
public JedisClient(String regionName, JedisPool jedisPool, int expiryInSeconds) {
log.debug("JedisClient created. regionName=[{}], jedisPool=[{}], expiryInSeconds=[{}]",
regionName, jedisPool, expiryInSeconds);
this.regionName = regionName;
this.jedisPool = jedisPool;
this.expiryInSeconds = expiryInSeconds;
}
/** 서버와의 통신 테스트, "PONG" 을 반환한다 */
public String ping() {
return run(new JedisCallback<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.ping();
}
});
}
/** db size를 구합니다. */
public Long dbSize() {
return run(new JedisCallback<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.dbSize();
}
});
}
/** 키에 해당하는 캐시 값이 존재하는지 확인합니다. */
public boolean exists(Object key) {
final byte[] rawRegion = rawRegion(key);
final byte[] rawKey = rawValue(key);
Long rank = run(new JedisCallback<Long>() {
@Override
public Long execute(Jedis jedis) {
return jedis.zrank(rawRegion, rawKey);
}
});
if (isTraceEnabled) log.trace("캐시 값이 존재하는지 확인합니다. key=[{}], exists=[{}]", key, (rank != null));
return rank != null;
}
/**
* 키에 해당하는 캐시 값을 구합니다.
*
* @param key 캐시 키
* @return 저장된 캐시 값, 없으면 null을 반환한다.
*/
public Object get(Object key) {
if (isTraceEnabled) log.trace("캐시 값을 조회합니다... key=[{}]", key);
final byte[] rawKey = rawKey(key);
byte[] rawValue = run(new JedisCallback<byte[]>() {
@Override
public byte[] execute(Jedis jedis) {
return jedis.get(rawKey);
}
});
return deserializeValue(rawValue);
}
/**
* 지정한 캐시 영역에 저장된 캐시의 키 집합을 반환합니다.
*
* @param regionName 캐시 영역명
* @return 캐시 영역에 저장된 모든 키 정보
*/
public Set<Object> keysInRegion(String regionName) {
if (isTraceEnabled) log.trace("영역에 해당하는 모든 키 값을 가져옵니다. regionName=[{}]", regionName);
final byte[] rawRegion = rawKey(regionName);
Set<byte[]> rawKeys = run(new JedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> execute(Jedis jedis) {
return jedis.zrange(rawRegion, 0, -1);
}
});
return deserializeKeys(rawKeys);
}
/**
* 지정한 키들의 값들을 한꺼번에 가져옵니다.
*
* @param keys 캐시 키 컬렉션
* @return 캐시 값의 컬렉션
*/
public List<Object> mget(Collection<? extends Object> keys) {
if (isTraceEnabled) log.trace("multi get... keys=[{}]", CollectionUtil.toString(keys));
if (keys == null || keys.size() == 0)
return new ArrayList<Object>();
final byte[][] rawKeys = rawKeys(keys);
List<byte[]> rawValues = run(new JedisCallback<List<byte[]>>() {
@Override
public List<byte[]> execute(Jedis jedis) {
return jedis.mget(rawKeys);
}
});
return deserializeValues(rawValues);
}
/**
* 캐시를 저장합니다.
*
* @param key 캐시 키
* @param value 캐시 값
*/
public final void set(Object key, Object value) {
set(key, value, -1);
}
/**
* 캐시를 저장합니다.
*
* @param key 캐시 키
* @param value 캐시 값
* @param timeoutInSeconds 유효 기간 (Seconds 단위)
*/
public final void set(Object key, Object value, long timeoutInSeconds) {
set(key, value, timeoutInSeconds, TimeUnit.SECONDS);
}
/**
* 캐시를 저장합니다.
*
* @param key 캐시 키
* @param value 캐시 값
* @param timeout 캐시 유효 시간
* @param unit 시간 단위 (기본은 seconds)
*/
protected void set(Object key, Object value, long timeout, TimeUnit unit) {
if (isTraceEnabled)
log.trace("캐시를 저장합니다... key=[{}], value=[{}]", key, value);
final byte[] rawKey = rawKey(key);
final byte[] rawValue = rawValue(value);
final byte[] rawRegion = rawRegion(key);
final int seconds = (int) unit.toSeconds(timeout);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.set(rawKey, rawValue);
tx.zadd(rawRegion, 0, rawKey);
if (seconds > 0) {
tx.expire(rawKey, seconds);
tx.expire(rawRegion, seconds);
}
}
});
}
public void delete(Object key) {
del(key);
}
/** 지정된 키의 항목으로 삭제합니다. */
public void del(Object key) {
if (isTraceEnabled) log.trace("캐시를 삭제합니다. key=[{}]", key);
final byte[] rawKey = rawKey(key);
final byte[] rawRegion = rawRegion(key);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.del(rawKey);
tx.zrem(rawRegion, rawKey);
}
});
}
/** 지정된 키의 항목으로 삭제합니다. */
public void mdel(Collection<? extends Object> keys) {
if (isTraceEnabled) log.trace("캐시를 삭제합니다. keys=[{}]", CollectionUtil.toString(keys));
if (keys == null || keys.size() == 0) return;
final byte[][] rawKeys = rawKeys(keys);
final byte[][] rawRegions = rawRegions(keys);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.del(rawKeys);
for (int i = 0; i < rawKeys.length; i++)
tx.zrem(rawRegions[i], rawKeys[i]);
}
});
}
public void deleteRegion(final String regionName) throws CacheException {
log.info("Region 전체를 삭제합니다... regionName=[{}]", regionName);
// Redis에서 한 Transaction 하에서 get / set 을 동시에 실행 할 수 없습니다. (복합 함수인 setnx 같은 것을 제외하고)
//
try {
final byte[] rawRegion = rawRegion(regionName);
Set<byte[]> keySet = run(new JedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> execute(Jedis jedis) {
return jedis.zrange(rawRegion, 0, -1);
}
});
if (keySet.size() > 0) {
final byte[][] rawKeys = keySet.toArray(new byte[keySet.size()][]);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.del(rawKeys);
tx.zremrangeByRank(rawRegion, 0, -1);
}
});
}
} catch (Throwable t) {
log.error("Region을 삭제하는데 실패했습니다.", t);
throw new CacheException(t);
}
}
public String flushDb() {
log.info("Redis DB 전체를 flush 합니다...");
return run(new JedisCallback<String>() {
@Override
public String execute(Jedis jedis) {
return jedis.flushDB();
}
});
}
/** 키를 byte[] 로 직렬화합니다 * */
@SuppressWarnings( "unchecked" )
private byte[] rawKey(Object key) {
return getKeySerializer().serialize(key);
}
@SuppressWarnings( "unchecked" )
private byte[][] rawKeys(Collection<? extends Object> keys) {
byte[][] rawKeys = new byte[keys.size()][];
int i = 0;
for (Object key : keys) {
rawKeys[i++] = getKeySerializer().serialize(key);
}
return rawKeys;
}
/** 키를 이용해 region 값을 직렬화합니다. */
@SuppressWarnings( "unchecked" )
private byte[] rawRegion(Object key) {
return getKeySerializer().serialize(getEntityName(key));
}
@SuppressWarnings( "unchecked" )
private byte[][] rawRegions(Collection<? extends Object> keys) {
byte[][] rawRegions = new byte[keys.size()][];
int i = 0;
for (Object key : keys) {
rawRegions[i++] = getKeySerializer().serialize(getEntityName(key));
}
return rawRegions;
}
private String getEntityName(Object key) {
if (key instanceof CacheKey)
return ((CacheKey) key).getEntityOrRoleName();
return regionName;
}
/** byte[] 를 key 값으로 역직렬화 합니다 */
private Object deserializeKey(byte[] rawKey) {
return getKeySerializer().deserialize(rawKey);
}
/** 캐시 값을 byte[]로 직렬화를 수행합니다. */
@SuppressWarnings( "unchecked" )
private byte[] rawValue(Object value) {
return getValueSerializer().serialize(value);
}
/** byte[] 를 역직렬화하여 원 객체로 변환합니다. */
private Object deserializeValue(byte[] rawValue) {
return getValueSerializer().deserialize(rawValue);
}
/**
* Redis 작업을 수행합니다.<br/>
* {@link redis.clients.jedis.JedisPool} 을 이용하여, {@link redis.clients.jedis.Jedis}를 풀링하여 사용하도록 합니다.
*/
private <T> T run(final JedisCallback<T> callback) {
final Jedis jedis = jedisPool.getResource();
try {
if (database != 0) jedis.select(database);
return callback.execute(jedis);
} catch (Throwable t) {
log.error("Redis 작업 중 예외가 발생했습니다.", t);
throw new RuntimeException(t);
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* 복수의 작업을 하나의 Transaction 하에서 수행하도록 합니다.<br />
* {@link redis.clients.jedis.JedisPool} 을 이용하여, {@link redis.clients.jedis.Jedis}를 풀링하여 사용하도록 합니다.
*/
private List<Object> runWithTx(final JedisTransactionalCallback callback) {
final Jedis jedis = jedisPool.getResource();
try {
if (database != 0) jedis.select(database);
Transaction tx = jedis.multi();
callback.execute(tx);
return tx.exec();
} catch (Throwable t) {
log.error("Redis 작업 중 예외가 발생했습니다.", t);
throw new RuntimeException(t);
} finally {
jedisPool.returnResource(jedis);
}
}
/** Raw Key 값들을 역직렬화하여 Key Set을 반환합니다. */
@SuppressWarnings( "unchecked" )
private Set<Object> deserializeKeys(Set<byte[]> rawKeys) {
return SerializationTool.deserialize(rawKeys, getKeySerializer());
}
/** Raw Value 값들을 역직렬화하여 Value List를 반환합니다. */
@SuppressWarnings( "unchecked" )
private List<Object> deserializeValues(List<byte[]> rawValues) {
return SerializationTool.deserialize(rawValues, getValueSerializer());
}
}

JedisClient 를 굳이 따로 만든 이유는 JedisPool 사용과 Transactional 작업이 반복적인 코드라 Jedis 만 사용하게되면 코드가 많아질까봐 Spring Data Redis 와 유사하게 만들었습니다.

물론 Jedis 의 모든 메소드를 지원하지는 않습니다. 캐시로 사용할 때 필요한 기능만을 구현했습니다.

이를 바탕으로 일반 저장소로 사용 가능하도록 확장할 수 있을겁니다.
특히 ShardedJedis 를 사용하려면, 다른 방법을 사용해야 합니다. 이 방식은 대용량 캐시나 저장소로 사용할 때 고려해 봐야 겠습니다.

참고로 JedisPool 설정 정보도 도움이 될겁니다.