기업용 솔루션을 만드는 경우 시간에 대한 계산이 참 많습니다.
특히, 일반적인 산술적 계산이 아니라 주말, 공휴일, 개인 휴가 (반차 포함), 팀 공가 등등을 고려한 작업 계획을 작성하려면 좀 복잡한 계산이 필요합니다.
이런 걸 일반적으로 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월 27일 월요일
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 자체에 대한 글을 써 볼까 합니다.
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 관련 자료를 정리해 봅니다.
특히 제게는 PostgreSQL 자체의 많은 기능도 좋지만, pgpool-ii 같이 좋은 load balancing 이 있다는 것과 엄청난 extensions 들이 굳이 상용을 쓸 필요 있을까 싶네요^^
몇가지 PostgreSQL 관련 자료를 정리해 봅니다.
- 한눈에 살펴보는 PostgreSQL
- PostgreSQL 용 유용한 Extensions
- pgpool-ii tutorial ( connection pool, load balancing, replication 제공)
- hstore 란?
- hstore for java, java-hstore-sample ( key-value for PostgreSQL)
- pgmemcache Setup and Usage (memcache 를 이용한 캐시)
- 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
xml 에서도 되는데 java configuration에서도 되겠지요?
그래서 구글링을 했더니 다행히, 몇번만에 해답을 찾았습니다.
원문: Spring annotations static injection tutorial
이 문서를 보고, 핵심은 원하는 클래스를 @Component로 지정하여 Spring Container에서 관리하도록하고, injection을 수행하도록 @Autowired를 지정하면 됩니다.
새로운 UnitOfWorks.java
UnitOfWorks 클래스는 @Component로 지정하고, @Autowired 해야 할 메소드는 static 이 아닌 instance method로 변경한 것으로 끝났다^^
마지막으로 Spring java configuration 에서 위의 UnitOfWorks를 ComponentScan 으로 등록해주면 됩니다.
StaticInjectionConfig.java
ComponentScan에서 UnitOfWorks class 를 지정해서 Component로 등록하게 하면, Bean으로 등록된 UnitOfWorkFactory가 injection이 됩니다^^
이로서 static field에 대해서도 java configuration에서 설정할 수 있게 되었습니다.
흠... Spring 에 대해 좀 더 공부해야겠네요...
xml 에서 작업하려면 MethodInvokingFactoryBean 을 이용하여 static method 를 호출하면 됩니다.
application-context.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 는 여러가지 장점 중에서도 질의어에 대한 지원 방식 풍부하다는 점이다.
이 있다.
SQL 에 익숙한 개발자들은 Criteria 의 가능성 및 효용성에 대해 느껴보지 못하고, 과소평가하는 경우가 많은데, 이 때 제가 아주 간단한 과제를 통해, Criteria 의 장점을 체험하게 합니다.
간단한 과제: 작업 기간에 대해 검색 조건으로 시작시각과 완료시각을 입력 받아 겹치는 기간에 있는 작업 정보를 조회하는 SQL 쿼리를 작성해 보시오.
위의 과제를 주면 99.9% 다 틀립니다. 0.1% 해결한다 하더라도, 너무 복잡해 다른 사람들은 사용하기에도 힘들 것입니다.
위 문제의 첫번째 난관은 기간에 해당하는 시작시각과 완료시각이 nullable 임을 인지하는 것입니다. 두 번째는 검색하고자하는 시작시각과 완료시각도 nullable 일 수 있다는 것입니다. 그럼 이런 조건 하에서 검색을 위한 질의어를 만드는데 경우의 수는 몇이나 될까요?
간단하게 2*2*2*2 = 2^4 이죠?
뭐 중딩 1차원 그래프까지 그려가면서 설명할 필요 없겠죠?
이걸 SQL 쿼리문으로 나타내라구요?
뭐 할 수는 있겠지요... 다만, 차라리 Criteria 라는 좋은 방식이 있으니 그걸 이용해 보라는 겁니다.
결론부터 말하면 다음과 같이 구현하면 됩니다.
OverlapCriteria
뭐 null 값인지 검사해서 알맞은 메소드를 호출하면 되네요^^
헐... 근데 내부에 보니 Between 과 InRange 라는 연산자를 사용하네요... 이건 뭐죠?
Between (일반적인 쿼리문에서는 A <= X <= B 이다. 경계값을 포함하는 closed 이다)
InRange 는 Between 과 대상과 검사 값이 반대일 뿐이다.
자 이제 Criteria 가 SQL 문보다 논리적으로 쉽게 구현되는지 알겠는가?
SQL문을 먼저 배웠다고, 좀 더 익숙하다고 그게 진리는 아닙니다.
java가 되었건, csharp 이 되었건, scala 가 되었건 익숙한 것보다 유용하게 표현할 수 있는 방식이 있다면 그게 진리입니다.
- Criteria
- 프로그램 언어로 빌더 패턴을 차용한 방식으로 질의를 빌드합니다.
- 사용자 정의의 Criteria 를 정의하여 사용할 수 있습니다.
- Criteria 를 실제 SQL 문으로 변환하는데 비용이 드는 단점이 있습니다.
- HQL (Hibernate Query Language)
- hibernate 고유의 질의어입니다.
- SQL 표준 문법과 유사하지만 새로 배워야 합니다.
- 변환에 드는 비용이 한번만 있고, 캐시하여 사용하므로 성능 상 유리합니다.
- SQL String
- 기존 표준 문법이므로 장단점이 따로 없습니다.
- ORM 에서도 그냥 사용해도 된다는 점
이 있다.
SQL 에 익숙한 개발자들은 Criteria 의 가능성 및 효용성에 대해 느껴보지 못하고, 과소평가하는 경우가 많은데, 이 때 제가 아주 간단한 과제를 통해, Criteria 의 장점을 체험하게 합니다.
간단한 과제: 작업 기간에 대해 검색 조건으로 시작시각과 완료시각을 입력 받아 겹치는 기간에 있는 작업 정보를 조회하는 SQL 쿼리를 작성해 보시오.
위의 과제를 주면 99.9% 다 틀립니다. 0.1% 해결한다 하더라도, 너무 복잡해 다른 사람들은 사용하기에도 힘들 것입니다.
위 문제의 첫번째 난관은 기간에 해당하는 시작시각과 완료시각이 nullable 임을 인지하는 것입니다. 두 번째는 검색하고자하는 시작시각과 완료시각도 nullable 일 수 있다는 것입니다. 그럼 이런 조건 하에서 검색을 위한 질의어를 만드는데 경우의 수는 몇이나 될까요?
간단하게 2*2*2*2 = 2^4 이죠?
뭐 중딩 1차원 그래프까지 그려가면서 설명할 필요 없겠죠?
이걸 SQL 쿼리문으로 나타내라구요?
뭐 할 수는 있겠지요... 다만, 차라리 Criteria 라는 좋은 방식이 있으니 그걸 이용해 보라는 겁니다.
결론부터 말하면 다음과 같이 구현하면 됩니다.
OverlapCriteria
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 지정한 범위 값이 두 속성 값 구간과 겹치는지를 알아보기 위한 질의어 */ | |
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)); | |
} | |
} |
헐... 근데 내부에 보니 Between 과 InRange 라는 연산자를 사용하네요... 이건 뭐죠?
Between (일반적인 쿼리문에서는 A <= X <= B 이다. 경계값을 포함하는 closed 이다)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 지정한 값이 두 속성 값 사이에 존재하는지 여부 */ | |
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 설정 정보도 도움이 될겁니다.
피드 구독하기:
글 (Atom)