2013년 3월 29일 금요일

Thrift 에서 Avro 로 급변경

현재 제작 중인 서비스의 내부 통신용 모듈을 뭘로 할까 하다가...
속도면에서 Apache Thrift 를 사용하려고 했는데, 어찌하다보니 알게된 Avro 가 더 괘찮은 해법이 되지 않을까 싶네요... 
더군다나 Avro RPC는 앞으로 나아갈 방향인 Hadoop 과 통합이 된다니 급 땡기네요.
특히나 대량의 비정형 메타 데이터가 전송되어야해서 Avro가 적격인 거 같습니다...

Avro, Thrift, Protocol Buffers 를 비교한 자료를 보면 어느정도 서로의 장단점을 알 수 있을 듯

2013년 3월 28일 목요일

Windows 8에 Couchbase 2.0 설치하기

Couchbase 2.0을 윈도우즈 8 에 설치했더니... localhost:8091이 안 열림... 서비스는 도는데... 초기화 설정을 못해서 아무것도 안됨...

Mac 에서는 잘 되는뎅....

구글링 해보니 Installing Couchbase 2.0 under Windows 8 에 자세히 나옴.

위와 같이 하면 설정까지는 잘 되는데, couchbase-node1 이 죽어 있음... 에구...
결국 Couchbase  는 Mac 에서만 테스트 중...

2013년 3월 26일 화요일

Spring 3.1 + Redis 를 이용한 Cache

이번 주에는 Spring 3.1 에서 지원하는 Cache 관련해서 많은 글을 썼는데, 요즘 가장 많이 사용되는 Redis 를 저장소로 사용하는 Cache 를 만들겠습니다.

Redis 를 구현하기 위해서 spring-data-redis 와 jedis 라이브러리를 사용했습니다.
jedis 만으로도 구현할 수 있지만, 편하게 spring-data-redis 의 RedisTemplate 를 사용하기로 했습니다.

우선 Redis 를 캐시 저장소로 사용하기 위해 환경설정을 합니다.

1. RedisCacheConfiguration.java

@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = UserRepository.class)
// @PropertySource("classpath:redis.properties")
public class RedisCacheConfiguration {
@Autowired
Environment env;
@Bean
public JedisShardInfo jedisShardInfo() {
return new JedisShardInfo("localhost");
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory(jedisShardInfo());
}
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}
@Bean
public RedisCacheManager redisCacheManager() {
return new RedisCacheManager(redisTemplate(), 300);
}
}

한가지 RedisCacheFactory를 생성할 때 주의할 점은 JedisShardInfo 로 생성해야지, JedisPoolingConfig나 기본 생성자로 생성 시에 RedisTemplate 에서 connection을 제대로 생성 못하는 버그가 있더군요 ㅠ.ㅠ 이 것 때문에 반나절을 허비...

2. RedisCacheManager.java

@Slf4j
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
private RedisTemplate redisTemplate;
private int expireSeconds;
public RedisCacheManager(RedisTemplate redisTemplate) {
this(redisTemplate, 300);
}
public RedisCacheManager(RedisTemplate redisTemplate, int expireSeconds) {
Guard.shouldNotBeNull(redisTemplate, "redisTemplate");
this.redisTemplate = redisTemplate;
this.expireSeconds = expireSeconds;
}
@Override
protected Collection<? extends Cache> loadCaches() {
Collection<Cache> caches = Lists.newArrayList();
for (String name : getCacheNames()) {
caches.add(new RedisCache(name, redisTemplate, expireSeconds));
}
return caches;
}
@Override
public Cache getCache(String name) {
synchronized (this) {
Cache cache = super.getCache(name);
if (cache == null) {
cache = new RedisCache(name, redisTemplate, expireSeconds);
addCache(cache);
}
return cache;
}
}
}


 3. RedisCache.java

@Slf4j
public class RedisCache implements Cache {
@Getter
private String name;
@Getter
private int expireSeconds;
private RedisTemplate redisTemplate;
public RedisCache(String name, RedisTemplate redisTemplate) {
this(name, redisTemplate, 300);
}
public RedisCache(String name, RedisTemplate redisTemplate, int expireSeconds) {
Guard.shouldNotBeEmpty(name, "name");
Guard.shouldNotBeNull(redisTemplate, "redisTemplate");
this.name = name;
this.redisTemplate = redisTemplate;
if (log.isDebugEnabled())
log.debug("MongoCache를 생성합니다. name=[{}], mongodb=[{}]", name, redisTemplate);
}
@Override
public Object getNativeCache() {
return redisTemplate;
}
public String getKey(Object key) {
return name + ":" + key;
}
@Override
public ValueWrapper get(Object key) {
Guard.shouldNotBeNull(key, "key");
if (log.isDebugEnabled())
log.debug("캐시 키[{}] 값을 구합니다...", key);
Object result = redisTemplate.opsForValue().get(getKey(key));
SimpleValueWrapper wrapper = null;
if (result != null) {
if (log.isDebugEnabled())
log.debug("캐시 값을 로드했습니다. key=[{}]", key);
wrapper = new SimpleValueWrapper(result);
}
return wrapper;
}
@Override
@SuppressWarnings("unchecked")
public void put(Object key, Object value) {
Guard.shouldNotBeNull(key, "key");
if (log.isDebugEnabled())
log.debug("캐시에 값을 저장합니다. key=[{}], value=[{}]", key, value);
redisTemplate.opsForValue().set(getKey(key), value, expireSeconds);
}
@Override
@SuppressWarnings("unchecked")
public void evict(Object key) {
Guard.shouldNotBeNull(key, "key");
if (log.isDebugEnabled())
log.debug("지정한 키[{}]의 캐시를 삭제합니다...", key);
try {
redisTemplate.delete(key);
} catch (Exception e) {
log.error("캐시 항목 삭제에 실패했습니다. key=" + key, e);
}
}
@Override
@SuppressWarnings("unchecked")
public void clear() {
if (log.isDebugEnabled())
log.debug("모든 캐시를 삭제합니다...");
try {
redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.flushAll();
return null;
}
});
} catch (Exception e) {
log.warn("모든 캐시를 삭제하는데 실패했습니다.", e);
}
}
}
view raw RedisCache.java hosted with ❤ by GitHub


RedisCache의 get / put 은 일반적으로 쓰는 opsForValue() 를 사용했습니다. 다른 것을 사용할 수도 있을텐데, 좀 더 공부한 다음에 다른 것으로 변경해 봐야 할 듯 합니다.
마지막에 clear() 메소드도 jedis 에는 flushDB(), flushAll() 메소드를 지원하는데, RedisTemplate에서는 해당 메소드를 expose 하지 않아 코드와 같이 RedisCallback 을 구현했습니다.

4. RedisCacheTest.java

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RedisCacheConfiguration.class})
public class RedisCacheTest {
@Autowired
RedisCacheManager redisCacheManager;
@Autowired
UserRepository userRepository;
@Test
public void clearTest() {
Assert.assertNotNull(redisCacheManager);
Cache cache = redisCacheManager.getCache("user");
Assert.assertNotNull(cache);
cache.clear();
Assert.assertNotNull(cache);
}
@Test
public void getUserFromCache() {
Stopwatch sw = new Stopwatch("initial User");
sw.start();
User user1 = userRepository.getUser("debop", 100);
sw.stop();
sw = new Stopwatch("from Cache");
sw.start();
User user2 = userRepository.getUser("debop", 100);
sw.stop();
Assert.assertEquals(user1.getUsername(), user2.getUsername());
}
@Test
public void componentConfigurationTest() {
Assert.assertNotNull(redisCacheManager);
Cache cache = redisCacheManager.getCache("user");
Assert.assertNotNull(cache);
cache.evict("debop");
Assert.assertNotNull(userRepository);
}
}


UserRepository는 전에 쓴 Spring 3.1 + EhCache 등의 글과 같은 코드라 생략했습니다.
Redis 관련은 Windows 에서는 구 버전만 지원하고, 신 버전은 linux 만 가능하더군요...
그래도 성능은 정평이 나있으니, HA 구성 시에는 가장 먼저 고려되어야 할 캐시 저장소라 생각됩니다.

저는 앞으로 hibernate  2nd cache provider for redis,  hibernate-ogm-redis 를 만들어 볼 예정입니다.

2013년 3월 25일 월요일

Spring 3.1 + Couchbase 를 이용하여 Cache 만들기

오늘 글 쓴 김에 다 써버려야 겠네요...
이번에는 Couchbase 를 저장소로 사용하여, Spring Cache 를 사용해 보도록 하겠습니다.
간략하게 Couchbase 에 대해 설명하면... Memcached + CouchDB 라고 보시면 되구요. 설정에 따라 메모리 저장소로 (Memcached), Document DB 형태(CouchDB) 로 사용할 수 있습니다.
Memcached 의 단순 캐시에서 DocumentDB 로 발전했다고 보시면 되고, Map Reduce 도 가능합니다.

하지만!!! 오늘은 그냥 캐시로 사용하려고 합니다.

1. CouchbaseCacheManager

@Slf4j
public class CouchbaseCacheManager extends AbstractTransactionSupportingCacheManager {
private CouchbaseClient couchbaseClient;
@Getter
@Setter
private int expireMillis = 0; // 0 : persist forever
public CouchbaseCacheManager(CouchbaseClient couchbaseClient) {
this(couchbaseClient, 0);
}
public CouchbaseCacheManager(CouchbaseClient couchbaseClient, int expireMillis) {
Guard.shouldNotBeNull(couchbaseClient, "couchbaseClient");
this.couchbaseClient = couchbaseClient;
this.expireMillis = expireMillis;
}
@Override
protected Collection<? extends Cache> loadCaches() {
Collection<Cache> caches = Lists.newArrayList();
for (String name : getCacheNames()) {
caches.add(new CouchbaseCache(name, couchbaseClient, expireMillis));
}
return caches;
}
@Override
public Cache getCache(String name) {
synchronized (this) {
Cache cache = super.getCache(name);
if (cache == null) {
cache = new CouchbaseCache(name, couchbaseClient, expireMillis);
addCache(cache);
}
return cache;
}
}
}


2. CouchbaseCache

@Slf4j
public class CouchbaseCache implements org.springframework.cache.Cache {
@Getter
private String name;
@Getter
private CouchbaseClient nativeCache = null;
@Getter
@Setter
private int expireMillis = 0; // msec (0 is persist forever)
public CouchbaseCache(String name, CouchbaseClient client, int expireMillis) {
Guard.shouldNotBeNull(name, "name");
Guard.shouldNotBeNull(client, "client");
this.name = name;
this.nativeCache = client;
this.expireMillis = expireMillis;
if (log.isDebugEnabled())
log.debug("CouchbaseCache 를 생성합니다. name=[{}], nativeCache=[{}]", name, nativeCache);
}
private String getKey(Object key) {
return name + "|+|" + key;
}
@Override
public ValueWrapper get(Object key) {
Guard.shouldNotBeNull(key, "key");
Object result = nativeCache.get(getKey(key));
SimpleValueWrapper wrapper = null;
if (result != null)
wrapper = new SimpleValueWrapper(result);
return wrapper;
}
@Override
public void put(Object key, Object value) {
Guard.shouldNotBeNull(key, "key");
OperationFuture<Boolean> setOp = nativeCache.set(getKey(key), expireMillis, value);
if (setOp.getStatus().isSuccess()) {
if (log.isDebugEnabled())
log.debug("객체를 캐시 키[{}]로 저장했습니다.", key);
} else {
if (log.isDebugEnabled())
log.debug("객체를 캐시 키[{}]로 저장하는데 실패했습니다. operation=[{}]", key, setOp);
}
}
@Override
public void evict(Object key) {
Guard.shouldNotBeNull(key, "key");
if (log.isDebugEnabled())
log.debug("delete cache item... key=[{}]", key);
nativeCache.delete(key.toString());
}
@Override
public void clear() {
if (log.isDebugEnabled())
log.debug("clear cache...");
nativeCache.flush();
}
}


3. CouchbaseCacheConfiguration

@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = {UserRepository.class})
@Slf4j
public class CouchbaseCacheConfiguration {
@Bean
public List<URI> couchbaseList() {
List<URI> uris = Lists.newArrayList(URI.create("http://localhost:8091/pools"));
return uris;
}
@Bean
public CouchbaseClient couchbaseClient() throws IOException {
return new CouchbaseClient(couchbaseList(), "default", "");
}
@Bean
public CouchbaseCacheManager couchbaseCacheManager() throws IOException {
return new CouchbaseCacheManager(couchbaseClient());
}
}

환경 설정은 각자 환경에 맞추면 됩니다만, default bucket 만 비밀번호 없이 접근이 가능합니다.
다른 bucket 을 사용하려면, 미리 만드시고, 보안을 설정하셔야 합니다.

4. Couchbase Admin Screenshot



이제 남은 건 Redis 만 남았네요... 요건 다음 기회에...

Spring 3.1 + MongoDB 를 이용한 Cache

앞서 EhCache, Memcached 는 일반적으로 메모리를 저장소로 사용합니다만, MongoDB 와 Couchbase 는 정보를 Document 로 관리하는 NoSQL DB라 볼 수 있습니다.

이런 의미로 단순 캐시로 쓰기에는 너무 많은 기능을 가지고 있다고 봐야합니다. 그런만큼 메모리에서만 작동하는 캐시시스템보다는 성능은 느립니다.
하지만 대용량 데이터나 캐시해야 할 양이 엄청 많다면? 그리고 한번 캐시한 거 영구 저장이 되면 좋겠다면? (물론 삭제 기능은 있고...)

에이 그런거라면 차라리 주 저장소를 Document DB로 바꾸는게 낫죠..

아 그렇긴 하네요... 근데, 개발자들이 NoSQL을 아주 단순히 쓰는 큰 이유는 RDBMS 처럼 관계형 데이터 처리에 익숙해서 key-value나 Document, grid 에 대한 처리에 난감해 한다는 거지요.  이 문제는 hibernate-ogm 으로 어떻게 해결하는지 향후 글을 써 보겠습니다.

우선 MongoDB 로도 가능하다는 보여 드리죠.

우선 제가 사용한 라이브러리는 spring-data-mongo, mongo-java-driver 입니다.

1. MongoCacheManager

@Slf4j
public class MongoCacheManager extends AbstractTransactionSupportingCacheManager {
private MongoTemplate mongoTemplate;
public MongoCacheManager(MongoTemplate mongoTemplate) {
Guard.shouldNotBeNull(mongoTemplate, "mongoTemplate");
this.mongoTemplate = mongoTemplate;
}
@Override
protected Collection<? extends Cache> loadCaches() {
Collection<Cache> caches = Lists.newArrayList();
for (String name : getCacheNames()) {
caches.add(new MongoCache(name, mongoTemplate));
}
return caches;
}
@Override
public Cache getCache(String name) {
synchronized (this) {
Cache cache = super.getCache(name);
if (cache == null) {
cache = new MongoCache(name, mongoTemplate);
addCache(cache);
}
return cache;
}
}
}

2. MongoCache

@Slf4j
public class MongoCache implements Cache {
private String name;
private MongoTemplate mongoTemplate;
public MongoCache(String name, MongoTemplate mongoTemplate) {
Guard.shouldNotBeEmpty(name, "name");
Guard.shouldNotBeNull(mongoTemplate, "mongoTemplate");
this.name = name;
this.mongoTemplate = mongoTemplate;
if (log.isDebugEnabled())
log.debug("MongoCache를 생성합니다. name=[{}], mongodb=[{}]", name, mongoTemplate.getDb().getName());
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return mongoTemplate;
}
@Override
public ValueWrapper get(Object key) {
Guard.shouldNotBeNull(key, "key");
CacheItem item = mongoTemplate.findOne(new Query(Criteria.where("key").is(key)), CacheItem.class, name);
Object result = null;
if (item != null) {
result = item.getValue();
if (log.isDebugEnabled())
log.debug("캐시 값을 로드했습니다. key=[{}], value=[{}]", key, result);
}
SimpleValueWrapper wrapper = null;
if (result != null)
wrapper = new SimpleValueWrapper(result);
return wrapper;
}
@Override
public void put(Object key, Object value) {
Guard.shouldNotBeNull(key, "key");
if (!mongoTemplate.collectionExists(name))
mongoTemplate.createCollection(name);
if (log.isDebugEnabled())
log.debug("캐시에 값을 저장합니다. key=[{}], value=[{}]", key, value);
if (get(key) == null)
mongoTemplate.insert(new CacheItem(key, value), name);
else
mongoTemplate.upsert(new Query(Criteria.where("key").is(key)),
Update.update("value", value),
name);
}
@Override
public void evict(Object key) {
Guard.shouldNotBeNull(key, "key");
mongoTemplate.remove(new Query(Criteria.where("key").is(key)), name);
}
@Override
public void clear() {
mongoTemplate.dropCollection(name);
}
@Getter
@Setter
public static class CacheItem implements Serializable {
private Object key;
private Object value;
public CacheItem() { this(null, null); }
public CacheItem(Object key, Object value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "CacheItem@{key=" + key + ", value=" + value + "}";
}
}
}
view raw MongoCache.java hosted with ❤ by GitHub


MongoCacheManager 는 별 내용 없구요... MongCache는 Spring 의 MongoTemplate 를 적극 이용하여, 저장/검색을 수행하였습니다.
상세 내용은 Spring Data Mongo 매뉴얼을 보시면 되겠습니다.

3. MongoCacheConfiguration

@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = {UserRepository.class})
@Slf4j
public class MongoCacheConfiguration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "debop4j_core_cache";
}
@Override
@Bean
public Mongo mongo() throws Exception {
return new Mongo("localhost");
}
@Bean
public MongoCacheManager mongoCacheManager() throws Exception {
return new MongoCacheManager(mongoTemplate());
}
}

너무 간단하죠? 나머지 테스트 코드는 앞 EhCache, Memcached 의 예와 똑같아서 생략하겠습니다.
MongoDB의 경우 Database를 지정해주고, 각 캐시명을 Collection 명으로 매핑합니다.
그럼 이만...

Spring 3.1 + Memcached 를 이용한 Cache 관리

전 글에 이어 Spring 3.1 캐시 저장소를 몇가지 늘려 봤습니다. Memcached, MongoDB, Couchbase 에 대해 만들어 봤습니다.
이번에는 Memcached 에 대해서만 설명해 보겠습니다.
Memcached 는 캐시들을 구분하는 개념이 없다는 게 하나의 특징이고, 분산 환경을 지원하므로, 직렬화/역직렬화를 통해 저장/로드 됩니다.
이것 때문에 당연히 In-Proc 인 ehcache 보다야 속도가 느리지만, HA 구성 시에는 캐시 서버로 좋은 선택이 될 수 있습니다.

그럼 먼저 MemcachedCacheManager 를 정의하면

@Slf4j
public class MemcachedCacheManager extends AbstractTransactionSupportingCacheManager {
@Getter
private MemcachedCache memcachedCache;
//protected MemcachedCacheManager() {}
public MemcachedCacheManager(MemcachedClient memcachedClient) {
Guard.shouldNotBeNull(memcachedClient, "memcachedClient");
memcachedCache = new MemcachedCache(memcachedClient);
}
public MemcachedCacheManager(MemcachedClient memcachedClient, int expireSeconds) {
Guard.shouldNotBeNull(memcachedClient, "memcachedClient");
memcachedCache = new MemcachedCache(memcachedClient, expireSeconds);
}
@Override
protected Collection<? extends Cache> loadCaches() {
super.getCacheNames();
return Sets.newHashSet(memcachedCache);
}
@Override
public Cache getCache(String name) {
return memcachedCache;
}
}

과 같습니다.

실제 캐시에 데이터를 저장/로드하는 Cache 는 다음과 같습니다.

@Slf4j
public class MemcachedCache implements Cache {
public static final int EXP_TIME = 100000;
@Getter
private String name = "default";
@Getter
private MemcachedClient nativeCache = null;
@Getter
private int expireSeconds;
// protected MemcachedCache() {}
public MemcachedCache(MemcachedClient client) {
this(client, EXP_TIME);
}
public MemcachedCache(MemcachedClient client, int expireSeconds) {
Guard.shouldNotBeNull(client, "client");
this.nativeCache = client;
this.expireSeconds = expireSeconds;
if (log.isDebugEnabled())
log.debug("MemcachedCache를 생성했습니다");
}
@Override
public ValueWrapper get(Object key) {
Guard.shouldNotBeNull(key, "key");
GetFuture<Object> result = nativeCache.asyncGet(key.toString());
SimpleValueWrapper wrapper = null;
try {
if (result.get() != null)
wrapper = new SimpleValueWrapper(result.get());
} catch (Exception ignored) {}
return wrapper;
}
@Override
public void put(Object key, Object value) {
Guard.shouldNotBeNull(key, "key");
if (!(key instanceof String)) {
log.error("Invalid key type: " + key.getClass());
return;
}
OperationFuture<Boolean> setOp = null;
setOp = nativeCache.set(key.toString(), expireSeconds, value);
if (log.isInfoEnabled()) {
if (setOp.getStatus().isSuccess()) {
log.info("객체를 캐시 키[{}]로 저장했습니다.", key);
} else {
log.info("객체를 캐시 키[{}]로 저장하는데 실패했습니다. operation=[{}]", key, setOp);
}
}
}
@Override
public void evict(Object key) {
if (key != null)
nativeCache.delete(key.toString());
}
@Override
public void clear() {
nativeCache.flush();
}
}


캐시 저장 시, 이미 있다면 update 되도록 set() 메소드를 사용합니다.

다음으로는 Spring 환경 설정을 보겠습니다. 뭐 특별한 것은 없고, MemcachedClient 를 생성해서 제공해 주면 됩니다.

@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = {UserRepository.class})
@Slf4j
public class MemcachedCacheConfiguration {
@Bean
public Transcoder<Object> transcoder() {
SerializingTranscoder transcoder = new SerializingTranscoder(Integer.MAX_VALUE);
transcoder.setCompressionThreshold(1024);
return transcoder;
}
/**
* MemcachedClient 를 제공해야 합니다
*/
@Bean
public MemcachedClient memcachedClient() {
try {
// 설정항목 : https://code.google.com/p/spymemcached/wiki/SpringIntegration
MemcachedClientFactoryBean bean = new MemcachedClientFactoryBean();
bean.setServers("localhost:11211"); // servers="host1:11211,host2:11211";
bean.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
bean.setTranscoder(transcoder());
bean.setOpTimeout(1000); // 1000 msec
bean.setHashAlg(DefaultHashAlgorithm.KETAMA_HASH);
bean.setLocatorType(ConnectionFactoryBuilder.Locator.CONSISTENT);
return (MemcachedClient) bean.getObject();
} catch (Exception ignored) {
throw new RuntimeException(ignored);
}
}
@Bean
public MemcachedCacheManager memcachedCacheManager() {
int timeoutInSeconds = 300;
return new MemcachedCacheManager(memcachedClient(), timeoutInSeconds);
}
}


마지막으로 테스트 코드는 ehcache 예와 같습니다.

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MemcachedCacheConfiguration.class})
public class MemcachedCacheTest {
@Autowired
MemcachedCacheManager cacheManager;
@Autowired
UserRepository userRepository;
@Test
public void getUserFromCache() {
Stopwatch sw = new Stopwatch("initial User");
sw.start();
User user1 = userRepository.getUser("debop", 100);
sw.stop();
sw = new Stopwatch("from Cache");
sw.start();
User user2 = userRepository.getUser("debop", 100);
sw.stop();
Assert.assertEquals(user1, user2);
}
@Test
public void componentConfigurationTest() {
Assert.assertNotNull(cacheManager);
Cache cache = cacheManager.getCache("user");
Assert.assertNotNull(cache);
Assert.assertNotNull(userRepository);
}
}

어떻습니까? 캐시와 관련해서 개발자가 최소한의 설정만으로 캐시를 효과적으로 사용할 수 있게 되었습니다.
물론 다양한 캐시를 쓸 수 있어, 용도에 맞게 사용하면 더 좋을 듯 합니다.


2013년 3월 24일 일요일

Spring 3.1 + EhCache 를 이용하여 캐시 사용하기

Spring 3.1 이상부터 Annotation을 이용하여, Cache 를 아주 쉽게 사용할 수 있습니다.
데이터를 처리하는 함수에서 @Cacheable 이라는 annotation을 사용하여, 반환되는 데이터를 캐시에 저장한다고 지정하기만 하면 됩니다.

Spring 3.1 Caching and @Cacheable 와 같은 좋은 예제가 많으니 그걸 참고하셔도 됩니다.

저도 비슷하게 함 예제를 만들어 봤습니다.
우선 캐시할 정보를 관리하는 UserRepository 라는 클래스를 구현했습니다. 보시다시피 @Repository 입니다.

@Repository
@Slf4j
public class UserRepository {
@Cacheable(value = "user", key = "#id")
public User getUser(String id) {
return getUser(id, 1000);
}
@Cacheable(value = "user", key = "#id")
public User getUser(String id, int favoriteMovieSize) {
User user = new User();
user.setUsername(id);
user.setPassword(id);
user.setEmail("sunghyouk.bae@gmail.com");
user.getHomeAddress().setPhone("999-9999");
user.getOfficeAddress().setPhone("555-5555");
for (int i = 0; i < favoriteMovieSize; i++)
user.getFavoriteMovies().add("Favorite Movie Number-" + i);
if (UserRepository.log.isDebugEnabled())
UserRepository.log.debug("Create User...");
return user;
}
}



처음 getUser() 메소드를 호출하게 되면,  User 인스턴스가 생성되어 반환되면서, 캐시에 자동 저장되고, 두번째 호출서부터는 캐시로부터 읽어드립니다.

정말 편하죠? Cache 와 관련된 코드가 설정만으로 끝낼 수 있다니^^ 물론 Cache 무효화나 key에 따라 다른 결과는 다른 키로 저장하기, 조건에 따라 캐싱하기 등등 더 많은 기능이 있습니다.

위와 같이 Cache 사용하기 위해서는 다음과 같이 Spring Configuration 을 정의합니다.

@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = {UserRepository.class})
@Slf4j
public class EhcacheConfiguration {
@Bean(name = "ehcache")
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean bean = new EhCacheManagerFactoryBean();
bean.setConfigLocation(new ClassPathResource("ehcache.xml"));
return bean;
}
@Bean(name = "cacheManager")
public EhCacheCacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheManagerFactoryBean().getObject());
}
@PostConstruct
public void postConstruct() {
EhcacheConfiguration.log.info("Cache Created");
}
}

보시다시피, EhCacheCacheManager 를 제공하는 Bean이 실제 쓰이는 CacheManager 입니다.

테스트는 Spring Configuration을 지정하고, UserRepository.getUser() 를 같은 값으로 두 번 호출하여, 두 번째가 캐시에서 읽어오는지 확인하면 됩니다.
(UserRepository에 보면 log에 쓰는 부분이 있죠? 두 번째에는 나타나지 않으면 됩니다.)

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {EhcacheConfiguration.class})
public class EhcacheTest {
@Autowired
EhCacheCacheManager cacheManager;
@Autowired
UserRepository userRepository;
@Test
public void getUserFromCache() {
Stopwatch sw = new Stopwatch("initial User");
sw.start();
User user1 = userRepository.getUser("debop", 100);
sw.stop();
sw = new Stopwatch("from Cache");
sw.start();
User user2 = userRepository.getUser("debop", 100);
sw.stop();
Assert.assertEquals(user1, user2);
}
}

자 테스트 코드는 위와 같고, 마지막으로 ehcache  설정은 다음과 같습니다.

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
updateCheck="false" monitoring="autodetect" dynamicConfig="false"
maxBytesLocalHeap="75%" name="debop4j-cache">
<diskStore path="java.io.tmpdir"/>
<cache name="user"
eternal="true"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"
statistics="false"
/>
</ehcache>
view raw ehcache.xml hosted with ❤ by GitHub


이 것을 응용해서 다른 Cache Provider 에 대해서도 작업을 하실 수 있을 것입니다. 요즘 가장 성능 좋은 것으로 사용되는 Redis 를 활용해도 되겠지요^^

hibernate-ogm configuration for spring framework

요즘 hibernate-ogm 을 제품에 적용하기 위해 공부하고 있습니다만, 아직 많은 활용이 안되나 봅니다. 개발 자료가 별로 없어, 소스와 테스트 코드를 분석하면서 공부하고 있습니다만...

역시 제 나름대로 테스트 환경부터 만들어서 공부하는 습관이 도움이 되네요...
그래서 제가 만든 환경에 대해 설명하고, 이 것을 바탕으로 hibernate-ogm을 제품에 쉽게 적용할 수 있도록 해 보겠습니다.

hibernate-ogm 도 hibernate 와 유사하게 환경설정을 합니다. 다만, hibernate 3.x 대의 Configuration을 이용하여 SessionFactory를 build 하는 것과 유사하게 SessionFactory를 빌드합니다. 즉 hibernate 4.x대와는 다르게 설정하네요.

또 한가지 확실하지는 않지만,  Configuration에서 Package 추가는 잘 안되고, annotationClass 는 제대로 되는군요...
=> 제가 잘못알고 알고 있었네요. package 추가 시에는 scan을 해줘야 하는데 그것은 새로 구현을 해야 하는 거더군요^^

우선 모든 Datastore (NoSql이라 그런지 Database 라 하지 않고 Datastore 라고 하는군요) 에 공통되는 부분을 구현한 GridDatastoreConfigBase.java 파일을 보면


package kr.debop4j.ogm.spring.cfg;
import kr.debop4j.data.hibernate.interceptor.StatefulEntityInterceptor;
import kr.debop4j.data.hibernate.repository.HibernateRepositoryFactory;
import kr.debop4j.data.hibernate.unitofwork.UnitOfWorkFactory;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.ogm.cfg.OgmConfiguration;
import org.hibernate.ogm.datastore.impl.DatastoreServices;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.dialect.GridDialect;
import org.hibernate.service.Service;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* hibernate-ogm 의 환경설정을 Spring Configuration으로 구현합니다.
*
* @author sunghyouk.bae@gmail.com
* 13. 3. 23. 오후 2:22
*/
@Configuration
@Slf4j
public abstract class GridDatastoreConfigBase {
/**
* DataStoreProvider 를 제공합니다.
*/
@Bean
public DatastoreProvider datastoreProvider() {
return (DatastoreProvider) getService(DatastoreProvider.class);
}
/**
* {@link GridDialect} 를 제공합니다.
*/
@Bean
public GridDialect gridDialect() {
return ((DatastoreServices) getService(DatastoreServices.class)).getGridDialect();
}
/**
* Hibernate SessionFactory 를 제공합니다.
*/
@Bean
public SessionFactory sessionFactory() {
if (log.isInfoEnabled())
log.info("hiberante-ogm 용 SessionFactory를 생성합니다...");
OgmConfiguration cfg = new OgmConfiguration();
for (String pkgName : getMappedPackageNames()) {
cfg.addPackage(pkgName);
}
for (Class annoatatedClass : getMappedEntities()) {
cfg.addAnnotatedClass(annoatatedClass);
}
cfg.setInterceptor(hibernateInterceptor());
cfg.setProperties(getHibernateOgmProperties());
if (log.isInfoEnabled())
log.info("hiberante-ogm 용 SessionFactory를 생성했습니다!!!");
return cfg.buildSessionFactory();
}
@Bean
public org.hibernate.Interceptor hibernateInterceptor() {
return new StatefulEntityInterceptor();
}
@Bean
public UnitOfWorkFactory unitOfWorkFactory() {
if (log.isInfoEnabled())
log.info("UnitOfWorkFactory를 생성합니다...");
UnitOfWorkFactory factory = new UnitOfWorkFactory();
factory.setSessionFactory(sessionFactory());
return factory;
}
@Bean
public HibernateRepositoryFactory hibernateRepositoryFactory() {
return new HibernateRepositoryFactory();
}
protected String getDatabaseName() {
return "debop4j_ogm_test";
}
protected String[] getMappedPackageNames() {
return new String[0];
}
protected Class[] getMappedEntities() {
return new Class[0];
}
protected Properties getHibernateProperties() {
Properties props = new Properties();
props.setProperty(Environment.USE_NEW_ID_GENERATOR_MAPPINGS, "true");
props.setProperty(Environment.HBM2DDL_AUTO, "none");
return props;
}
protected Properties getHibernateOgmProperties() {
return getHibernateProperties();
}
protected org.hibernate.service.Service getService(Class<? extends Service> serviceImpl) {
SessionFactoryImplementor sessionFactory = sfi();
ServiceRegistryImplementor serviceRegistry = sessionFactory.getServiceRegistry();
return serviceRegistry.getService(serviceImpl);
}
protected SessionFactoryImplementor sfi() {
return (SessionFactoryImplementor) sessionFactory();
}
}

public SessionFactory sessionFactory() {...} 는 아주 익숙한 코드지요? 단지 Configuration class가 hibernate 것이 아닌 hibernate-ogm의 OgmConfiguration을 사용한다는 것만 다릅니다.

아래의 getDatabaseName(), getMappedPackageNames(), getMappedEntities() 메소드는 각자 Datastore와 제품에 따라 재정의하면 됩니다.
그 밑에 getHibernateProperties() 와 getHibernateOgmProperties() 는 각각 Hibernate 설정과 Hibernate-Ogm 설정을 추가할 수 있습니다. 이 부분도 재정의를 통해 추가하시면 됩니다.

그럼 MongoDB를 사용하는 환경설정은? 위의 GridDataStoreConfigBase를 상속하여 몇가지 메소드를 재정의만 하면 됩니다.

package kr.debop4j.ogm.spring.cfg.mongodb;
import kr.debop4j.ogm.spring.cfg.GridDatastoreConfigBase;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.ogm.datastore.mongodb.AssociationStorage;
import org.hibernate.ogm.datastore.mongodb.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* MongoDB를 Datastore로 사용하는 hibernate-ogm 에 대한 환경설정입니다.
*
* @author sunghyouk.bae@gmail.com
* 13. 3. 23. 오후 3:53
*/
@Configuration
@Slf4j
public abstract class MongoDBConfigBase extends GridDatastoreConfigBase {
public static final String MONGODB_DATASTORE_PROVIDER = "org.hibernate.ogm.datastore.mongodb.impl.MongoDBDatastoreProvider";
protected Properties getHibernateOgmProperties() {
Properties props = getHibernateProperties();
props.setProperty("hibernate.ogm.datastore.provider", MONGODB_DATASTORE_PROVIDER);
props.put(Environment.MONGODB_DATABASE, getDatabaseName());
props.put(Environment.MONGODB_TIMEOUT, 200);
props.put(Environment.MONGODB_ASSOCIATIONS_STORE, getAssociationStorage().name());
return props;
}
protected AssociationStorage getAssociationStorage() {
return AssociationStorage.COLLECTION;
}
@Bean
public javax.transaction.TransactionManager transactionManager() {
return com.arjuna.ats.jta.TransactionManager.transactionManager();
}
}

MongoDB 만의 설정을 보면 database 명을 설정해줘야 하고, 엔티티의 저장 방식을 설정해주게
됩니다. AssociationStorage enum 값의 설명을 보면 ...

/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* JBoss, Home of Professional Open Source
* Copyright 2012 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.hibernate.ogm.datastore.mongodb;
/**
* Defines the various association storage strategies
*
* @author Alan Fitton <alan at eth0.org.uk>
* @author Emmanuel Bernard <emmanuel@hibernate.org>
*/
public enum AssociationStorage {
/**
* Store the association info in a unique MongoDB collection for all associations
*/
GLOBAL_COLLECTION,
/**
* Store the association in a dedicated MongoDB collection per association
*/
COLLECTION,
/**
* Store association information from within the entity
*/
IN_ENTITY
}

입니다. Entity 들을 전역 컬렉션에 저장, 지정한 컬렉션에 저장, 엔티티별로 저장과 같이 3가지 방식이 있습니다.

뭐 Lazy Initialization을 많이 사용하려면 엔티티별로 저장하고, 일반적으로는 컬렉션에 저장하는게 좋으리라 생각됩니다.

자 그럼 실제 테스트 시에 사용할 Configuration을 보면은

package org.hibernate.ogm.test.mongodb;
import kr.debop4j.ogm.spring.cfg.mongodb.MongoDBConfigBase;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.ogm.test.mongodb.model.Module;
import org.hibernate.ogm.test.mongodb.model.Project;
import org.springframework.context.annotation.Configuration;
/**
* MongoDB 를 DataStore로 사용하는 Configuration
*
* @author sunghyouk.bae@gmail.com
* 13. 3. 23. 오후 2:31
*/
@Configuration
@Slf4j
public class MongoDBConfiguration extends MongoDBConfigBase {
@Override
protected String getDatabaseName() {
return "debop4j_ogm_test";
}
@Override
protected String[] getMappedPackageNames() {
return new String[]{
Project.class.getPackage().getName(),
};
}
@Override
protected Class[] getMappedEntities() {
return new Class[]{
Module.class,
Project.class
};
}
}

과 같습니다. DB명을 지정하고, 매핑된 엔티티를 지정해 주는 것으로 끝납니다^^
이제부터는 위의 Configuration을 이용하여 테스트 코드를 작성하여 테스트를 수행하면됩니다

hibernate-ogm은 현재까지 NoSQL을 단순 저장소로 밖에 활용 못한 것을 Object Grid 방식으로 사용할 수 있도록 한 차원 업그래이드된 방법을 제공합니다.
이를 통해 앞으로는 RDBMS 뿐 아니라 NoSQL도 특정 제품에 구애받지 않고 사용할 수 있었으면 합니다.



2013년 3월 19일 화요일

hibernate-validator 사용 시

Business Application 개발 시, DDD를 적용하는 것은 거의 표준이고, Domain Model 을 전 Layer에 걸쳐 사용하는 것이 대세입니다.
hibernate 를 사용하는 경우는 hibernate-validator, hibernate-search 등을 같이 사용하게 되면, 상당히 많은 부분에서 업무 로직 등을 손쉽게 구현할 수 있습니다.

특히 hibernate-validator 는 Model의 속성 값의 제약 조건을 annotation을 이용하여 손쉽게 정의할 수 있습니다.

@NotEmpty
public String getName() { ... }

이라 하면,  엔티티의 name 속성 값은 빈 문자열이면 안된다는 뜻입니다. DB에 insert, update 시에 위의 검사 조건으로 자동 검사가 가능합니다.

자세한 내용은

Hibernate Validator Reference 4.3.1 Html Single
Hibernate Validator Reference Pdf

을 보시는게 ...

기본 제공되는 부가 제약 조건으로는

@AssertFalse, @AssertTrue, @DecimalMax, @DecimalMin, @Digits(integer=,fraction=), @Future, @Max, @Min, @NotNull, @Null, @Past, @Pattern(regex=,flag=), @Size(min=,max=), @Valid
가 있습니다.


부가 제약 조건으로는

@CreditCardNumber, @Email, @Length(min=, max), @ModChceck, @NotBlank,  @NotEmpty, @Range(min,max), @SafeHtml, @ScriptAssert, @URL 등이 있습니다.

위의 Validation이 굳이 필요한가? DB에도 Constraint 를 정의할 수 있는데? 라고 한다면...

Validation은 다음과 같은 단계에 모두 필요합니다.
1. Presentation Layer 에서 입력 작업 시
2. Business Layer 또는 Service Layer 에서 Entity 작업 시
3. Data Access Layer 에서 DB에 insert, update 시

에 하게 되면 굳이 DB에서 작업이 필요없습니다.
그리고 DB는 되도록 부가 작업을 안하게 하는 것이 성능 상 더 좋습니다.

또 향후 저장소를 RDBMS가 아닌 NoSQL을 사용하고자 할 경우에는 hibernate-ogm 을 사용하게 되면 다른 것은 하나도 바꿀 필요 없고, 환경설정만 변경하면 됩니다^^

2013년 3월 17일 일요일

NoSQL 용 ORM인 hibernate-ogm을 소개합니다.

OOP 와 RDBMS 의 매핑을 위해 탄생한 ORM ( Object Relational Mapping ) 이 있다면, OOP 와 NoSQL 의 매핑을 위한 OGM ( Object Grid Mapping ) 이 탄생하는 것은 자연스러운 현상이겠죠^^
ORM의 대표 주자인 JBoss 의 hibernate 를 기반으로 OGM 용 라이브러리인 hibernate-ogm 이 있습니다. 현재 4.0.0 Beta 2 까지 나왔고, 공식 대상 NoSQL은 EhCache, Infinispan, MongoDB 이고, 비공식적으로는  Redis, HBase, Cassandra 도 지원 또는 개발 중입니다.

hibernate-ogm 의 장점 중에 또 한가지는 hibernate-search 도 결합하여, lucene의 인덱스 정보를 no-sql 에 저장이 가능하도록 했습니다. 이렇게되면, 검색 서비스도 분산 환경에서는 확실히 확장성을 보장해 주겠지요.

다음 발표자료를 보시면 좀 더 잘 아실 수 있을 겁니다.


Spring-data 에서도 NoSQL 을 위한 다양한 라이브러리가 있지만, 제가 hibernate 의 heavy user 이라서, no-sql을 사용할 때도 hibernate 를 그대로 사용할 수 있으면 좋겠다 싶었는데, 작년부터 알게되었지만, 이제사 제대로 사용하게 되었습니다.

혹시 관심있으신 분들도 한번 시도해보시기 바랍니다. No-SQL 에서 relation 관련 기존 관습을 타파하지 못해 적용하는데 실패하는 경우도 많고, 실전으로 사용하는데, 신뢰가 없을 때 pilot 으로 시도해 볼 수도 있을 겁니다.

한가지 아쉬운 점은 JBoss 가 너무 자기중심적으로 Infinispan 을 미는 게 확산에 걸림돌이 되지 않나 싶네요^^.
하지만 곧 7월에 책 (Pro Hibernate and MongoDB) 도 나오니, 좀 더 확산이 되지 않을까 싶네요.

2013년 3월 15일 금요일

Spring 을 이용한 hibernate 환경설정

진부한 내용일 수 있지만, Spring 을 이용해 hibernate 에 대한 환경 설정에 대한 정보가 대부분 단편적이거나, 하나의 DB에 대해서만 설명한 글이 대부분이라...
hibernate 가 여러 DB를 동시에 만족 시킬 수 있음을 강조하고, 솔루션을 만들 때 여러 DB에 대해 만족할 수 있도록 테스트를 쉽게 하기 위해 간단하게나마 hibernate 설정을 spring 의 @Configuration을 이용하여 제작해 보았습니다.

우선 모든 DB에 대해 공통적으로 적용되는 부분에 대해 다음과 같이 정의했습니다.

package kr.debop4j.data.hibernate.springconfiguration;
import kr.debop4j.core.tools.StringTool;
import kr.debop4j.data.hibernate.forTesting.UnitOfWorkTestContextBase;
import kr.debop4j.data.hibernate.interceptor.MultiInterceptor;
import kr.debop4j.data.hibernate.interceptor.StatefulEntityInterceptor;
import kr.debop4j.data.hibernate.interceptor.UpdateTimestampedInterceptor;
import kr.debop4j.data.hibernate.repository.HibernateRepositoryFactory;
import kr.debop4j.data.hibernate.unitofwork.UnitOfWorkFactory;
import kr.debop4j.data.jdbc.JdbcTool;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
/**
* hibernate 의 환경설정을 spring framework의 bean 환경설정으로 구현했습니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Slf4j
public abstract class HibernateConfigBase {
@Getter
@Setter
private UnitOfWorkTestContextBase testContext;
abstract protected String getDatabaseName();
abstract protected String[] getMappedPackageNames();
protected Properties hibernateProperties() {
Properties props = new Properties();
props.put(Environment.FORMAT_SQL, "true");
props.put(Environment.HBM2DDL_AUTO, "create"); // create | spawn | spawn-drop | update | validate
props.put(Environment.SHOW_SQL, "true");
props.put(Environment.RELEASE_CONNECTIONS, ConnectionReleaseMode.ON_CLOSE);
props.put(Environment.AUTOCOMMIT, "true");
props.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
props.put(Environment.STATEMENT_BATCH_SIZE, "50");
return props;
}
protected DataSource buildDataSource(String driverClass, String url, String username, String password) {
return JdbcTool.getDataSource(driverClass, url, username, password);
}
protected DataSource buildEmbeddedDataSource() {
EmbeddedDatabaseFactoryBean bean = new EmbeddedDatabaseFactoryBean();
bean.afterPropertiesSet();
return bean.getObject();
}
@Bean(destroyMethod = "close")
abstract public DataSource dataSource();
/**
* factoryBean 에 추가 설정을 지정할 수 있습니다.
*/
protected void setupSessionFactory(LocalSessionFactoryBean factoryBean) { }
@Bean
public SessionFactory sessionFactory() {
if (log.isInfoEnabled())
log.info("SessionFactory Bean을 생성합니다...");
LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
String[] packageNames = getMappedPackageNames();
if (packageNames != null) {
log.info("hibernate용 entity를 scan합니다. packages=[{}]", StringTool.listToString(packageNames));
factoryBean.setPackagesToScan(packageNames);
}
factoryBean.setHibernateProperties(hibernateProperties());
factoryBean.setDataSource(dataSource());
factoryBean.setEntityInterceptor(hibernateInterceptor());
// Drived class에서 추가 작업을 수행할 수 있도록 합니다.
setupSessionFactory(factoryBean);
try {
factoryBean.afterPropertiesSet();
if (log.isInfoEnabled())
log.info("SessionFactory Bean을 생성했습니다!!!");
return factoryBean.getObject();
} catch (IOException e) {
throw new RuntimeException("SessionFactory 빌드에 실패했습니다.", e);
}
}
@Bean
public HibernateTransactionManager transactionManager() {
return new HibernateTransactionManager(sessionFactory());
}
@Bean
public MultiInterceptor hibernateInterceptor() {
MultiInterceptor interceptor = new MultiInterceptor();
interceptor.getInterceptors().add(statuefulEntityInterceptor());
interceptor.getInterceptors().add(updateTimestampedInterceptor());
return interceptor;
}
@Bean
public StatefulEntityInterceptor statuefulEntityInterceptor() {
return new StatefulEntityInterceptor();
}
@Bean
public UpdateTimestampedInterceptor updateTimestampedInterceptor() {
return new UpdateTimestampedInterceptor();
}
@Bean
public UnitOfWorkFactory unitOfWorkFactory() {
UnitOfWorkFactory factory = new UnitOfWorkFactory();
factory.setSessionFactory(sessionFactory());
return factory;
}
@Bean
public HibernateRepositoryFactory hibernateRepositoryFactory() {
return new HibernateRepositoryFactory();
}
}

public SessionFactory sessionFactory() {...} 함수가 가장 중요하고, 나머지는 뭐 별로...
당연히 hibernate 설정 클래스이니까... ㅎㅎ
sessionFactory를 만드는데, 여러가지 필요한 설정들을 지정하게 하는데, 미리 정의된 부분도 있고, 사용자가 더 필요한 부분은 "setupSessionFactory(factoryBean)" 위임 메소드를 이용하여, 추가로 정의 할 수 있도록 했습니다.

그럼 대표적으로 사용하는 DB인 HSQL, MySQL, PostgreSQL 에 대한 기본 환경설정 클래스를 보겠습니다.

1. HSql 메모리 DB 사용 (단순 테스트시 유용)
package kr.debop4j.data.hibernate.springconfiguration;
import kr.debop4j.data.hibernate.forTesting.DatabaseEngine;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
import java.util.Properties;
/**
* HSql 을 DB로 사용하는 Hibernate Configuration
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
public abstract class HSqlConfigBase extends HibernateConfigBase {
public DatabaseEngine getDatabaseEngine() {
return DatabaseEngine.HSql;
}
public String getDatabaseName() {
return "mem";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.hsqldb.jdbcDriver",
"jdbc:hsqldb:" + getDatabaseName() + ":test",
"sa",
"");
}
protected Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.HSQLDialect");
return props;
}
}

2. MySQL
package kr.debop4j.data.hibernate.springconfiguration;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
/**
* kr.debop4j.data.hibernate.springconfiguration.MySqlDbConfiguration
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Configuration
@EnableTransactionManagement
public abstract class MySqlConfigBase extends HibernateConfigBase {
@Override
public String getDatabaseName() {
return "hibernate";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("com.mysql.jdbc.Driver",
"jdbc:mysql://localhost/" + getDatabaseName(),
"root",
"root");
}
@Override
@Bean
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.MySQL5InnoDBDialect");
return props;
}
}

3. PostgreSql
package kr.debop4j.data.hibernate.springconfiguration;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
/**
* PostgreSQL DB를 사용하는 Hibernate 용 환경설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Configuration
@EnableTransactionManagement
public abstract class PostgreSqlConfigBase extends HibernateConfigBase {
@Override
public String getDatabaseName() {
return "hibernate";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.postgresql.Driver",
"jdbc:postgresql://localhost/" + getDatabaseName() + "?Set=UTF8",
"root",
"root");
}
@Bean
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.PostgreSQL82Dialect");
return props;
}
}

4. PostgreSql with pgpool-II
package kr.debop4j.data.hibernate.springconfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* PostgreSQL 용 ConnectionPool과 Replication을 제공하는 PgPool 로 Connection을 만듭니다. (포트 9999를 사용합니다)
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 26.
*/
@Configuration
@EnableTransactionManagement
public abstract class PgPoolConfigBase extends PostgreSqlConfigBase {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.postgresql.Driver",
"jdbc:postgresql://localhost:9999/" + getDatabaseName() + "?Set=UTF8",
"root",
"root");
}
}

과 같다.

아주 사소한 port 번호 같은 것은 기본 값을 사용했다.
뭐 각각 다른 것은 DB명, ConnectionString, Dialect 등이다. 별 차이 없지만, 이렇게 구조적으로  상속체계를 해 놓으면, 실전에서 아주 편리하다.

실제 사용하는 예는 HSql 과 PostgreSql 의 예를 들어보자.

1. HSql 메모리 DB를 이용한 환경 설정 (2nd 캐시도 적용됨)
package kr.debop4j.access;
import kr.debop4j.access.model.organization.Company;
import kr.debop4j.access.model.product.Product;
import kr.debop4j.access.model.workcalendar.WorkCalendar;
import kr.debop4j.data.hibernate.springconfiguration.HSqlConfigBase;
import org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Properties;
/**
* HSql 메모리 DB를 사용하는 Hibernate 환경 설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 7.
*/
@Configuration
@EnableTransactionManagement
public class UsingHSqlConfiguration extends HSqlConfigBase {
@Override
protected String[] getMappedPackageNames() {
return new String[]{
Company.class.getPackage().getName(),
Product.class.getPackage().getName(),
WorkCalendar.class.getPackage().getName(),
};
}
@Override
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.USE_SECOND_LEVEL_CACHE, true);
props.put(Environment.USE_QUERY_CACHE, true);
props.put(Environment.CACHE_REGION_PREFIX, "debop4j-access");
props.put(Environment.CACHE_REGION_FACTORY, SingletonEhCacheRegionFactory.class.getName());
props.put(Environment.CACHE_PROVIDER_CONFIG, "classpath:ehcache.xml");
return props;
}
}


2. PostgreSql 의 "HAccess" DB 를 이용한 환경 설정 (2nd Cache도 적용됨)
package kr.debop4j.access;
import kr.debop4j.access.model.organization.Company;
import kr.debop4j.access.model.organization.CompanyCode;
import kr.debop4j.access.model.product.Product;
import kr.debop4j.access.model.workcalendar.WorkCalendar;
import kr.debop4j.data.hibernate.springconfiguration.PostgreSqlConfigBase;
import org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Properties;
/**
* PostgreSQL DB를 사용하는 Hibernate 환경설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 7.
*/
@Configuration
@EnableTransactionManagement
public class UsingPostgreSqlConfiguration extends PostgreSqlConfigBase {
@Override
public String getDatabaseName() {
return "HAccess";
}
@Override
protected String[] getMappedPackageNames() {
return new String[]{
CompanyCode.class.getPackage().getName(),
Company.class.getPackage().getName(),
Product.class.getPackage().getName(),
WorkCalendar.class.getPackage().getName(),
};
}
@Override
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.USE_SECOND_LEVEL_CACHE, true);
props.put(Environment.USE_QUERY_CACHE, true);
props.put(Environment.CACHE_REGION_FACTORY, SingletonEhCacheRegionFactory.class.getName());
props.put(Environment.CACHE_PROVIDER_CONFIG, "classpath:ehcache.xml");
return props;
}
}

이 것으로 끝나는게 아니고, Application 용 환경설정에서는 다음과 같이 위의 두 가지 환경설정 Class 를 입맛에 따라 Import 해서 테스트를 수행하면 된다.
package kr.debop4j.access;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* kr.debop4j.access.AppConfig
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 2.
*/
@Configuration
@EnableTransactionManagement
@ComponentScan({"kr.debop4j.access.repository", "kr.debop4j.access.service"})
@Import({UsingPostgreSqlConfiguration.class})
public class AppConfig {
// @ComponentScan 으로 @Repository, @Service 는 정의할 필요 없다.
}
view raw AppConfig.java hosted with ❤ by GitHub

정말 쉽죠?

이제 실제 단위 테스트 클래스에서 AppConfig를 지정해서 테스트를 수행하시면 됩니다^^

ORM의 장점 중에 여러 DB에 대해 일관된 작업이 가능하므로, 솔루션을 만들때는 최선의 선택이라 생각하는지라, 위와 같은 설정으로 개발을 수행합니다. (비록 한가지 DB만을 대상으로 하는 시스템이라도... 향후 어떻게 될지 모르고, 코드 재활용성도 생각하고^^)

2013년 3월 12일 화요일

hibernate 가 identity column을 clustered index 로 만들지 않는 이유

오랜만에 글을 쓰네요...
요즘 프로젝트 문서 작성을 주로 하는 바람에 글을 쓸 밑천이 없었네요 ㅎㅎ
이런 얕은 내공이라구...

오늘은 그동안 궁금했던 "왜 SQL Server 는 primary key가 clustered index 인데, PostgreSQL, MySQL 등은 clustered index 가 아닌가?" 에 대해 그냥 RDBMS 마다 속 사정이 있겠지... 하고 넘어갔었는데, hibernate 로 매핑을 수행하던 도중에 혹시나 하는 마음에 구글링을 해 봤습니다...

이런 논의가 hibernate 에서도 진행되었었네요...

원문( https://hibernate.onjira.com/browse/HHH-3305 ) 을 보면, Clustered Index 자체가 성능이 좋긴 하고, SQL Server 는 Primary Key가 기본으로 Clustered Index 지만

1. 다른 DB들은  Primary Key가 Clustered Index 가 아니다.
2. 유지 관리에 애로점이 있다.
3. SQL Server 는 DBA 기술이 없는 작은 조직에 초점이 맞춰져있다.
4. Clustered Index 를 삭제하고자 할때에는 부가적인 작업이 필요하다.

흠 이런 이유 때문인지는 모르지만, SQL Server 는 hiberante mapping test 시에 association으 변경 될 때 drop 시 실패하는 경우가 다반사입니다. oracle, postgresql은 괜찮구요. 물론 drop cascade 를 지원하기 때문이기도 하구요...

그동안 습관적으로 Primary Key는 Clustered Index 여야만 해!!! 라는 고정관념을 깨게 되었습니다.

뭐 꼭 필요하다면, DBA 가 튜닝 단계에서 적용해주는게 합당하겠죠^^