2013년 7월 30일 화요일

IntelliJ 에서 TODO 외에 것을 추가해보자.

VS.NET 에 Resharper 를 사용할 때에는 TODO 외에 BUG, HINT, NOTE 등을 추가하여, 개발 시에 주의할 점을 특별히 기록하곤 했습니다만, IntelliJ 에서는 TODO 만 되는 줄 알았네요. 우연히 다른 것 때문에 보다보니 TODO 외에도 추가할 수 있더군요.


IntelliJ Settings -> TODO

보시면 todo, fixme 가 기본적으로 제공되고, 정규식으로 comment 에서 위의 글자로 시작하면 데코레이션을 수행해 줍니다.
전 여기에 note, bug, hint 를 추가했습니다.

에디터에서 추가한 것이 어떻게 나오나 봤습니다... 짜잔~




이쁘게 나왔죠? 원하는 색상도 가능하고, 스크롤 바에 표시도 가능합니다.
제가 Note 나 Hint 를 많이 쓰고 싶었는데 이제서야 알게 되었네요.

2013년 7월 21일 일요일

MySql Master-Slave Replication 에서 JPA 사용하기

전편의 MySql Master-Slave Replication 에서 Hiberante 사용하기 에서 몇가지 개선 사항과 JPA에도 적용하기 위해 몇가지 사항을 바꿨습니다.

1. Transactional annotation 의 readOnly 가 true 일때만 point cut 하기
2. Session.connection() 이 폐기될 것이므로 doWork() 를 사용하도록 하기 입니다.

MySqlConnectionInterceptor.java
package kr.hconnect.data.jpa.mysql;
import lombok.Getter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.hibernate.Session;
import org.hibernate.jdbc.Work;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.sql.Connection;
import java.sql.SQLException;
/**
* MySQL Replication 환경 (Master-Slave)에서
* {@link org.springframework.transaction.annotation.Transactional#readOnly()} 이 true로 정의된 Method에 대해서는
* Slave 서버로 접속하기 위해, {@link java.sql.Connection#isReadOnly()}의 속성을 true로 변경하여 작업을 수행하도록 합니다.
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 7. 21. 오후 3:20
*/
@Aspect
@Component
public class MySqlConnectionInterceptor {
private static final Logger log = LoggerFactory.getLogger(MySqlConnectionInterceptor.class);
private static final boolean isTraceEnabled = log.isTraceEnabled();
@PersistenceContext
EntityManager em;
/** `@Transactional` 이 있는 메소드를 intercept 해서 readOnly 값에 따라 MySQL의 Master / Slave 서버를 구분합니다. */
@Around(value = "@annotation(transactional) if transactional.readOnly()", argNames = "pjp, transactional")
public Object proceed(final ProceedingJoinPoint pjp, final Transactional transactional) throws Throwable {
if (log.isTraceEnabled())
log.trace("읽기전용 작업을 수행하기 위해 현 connection를 readonly로 설정합니다...");
Session session = em.unwrap(Session.class);
ConnectionReadOnlyWork readOnlyWork = new ConnectionReadOnlyWork();
try {
session.doWork(readOnlyWork);
return pjp.proceed();
} finally {
session.doWork(new RestoreConnectionWork(readOnlyWork));
}
}
static class ConnectionReadOnlyWork implements Work {
@Getter boolean autoCommit;
@Getter boolean readOnly;
@Override
public void execute(Connection connection) throws SQLException {
this.autoCommit = connection.getAutoCommit();
this.readOnly = connection.isReadOnly();
connection.setAutoCommit(false);
connection.setReadOnly(true);
}
}
static class RestoreConnectionWork implements Work {
@Getter boolean autoCommit;
@Getter boolean readOnly;
public RestoreConnectionWork(ConnectionReadOnlyWork readOnlyWork) {
this.autoCommit = readOnlyWork.isAutoCommit();
this.readOnly = readOnlyWork.isReadOnly();
}
@Override
public void execute(Connection connection) throws SQLException {
connection.setAutoCommit(autoCommit);
connection.setReadOnly(readOnly);
if (log.isTraceEnabled())
log.trace("읽기전용 작업을 수행하고, connection의 원래 설정으로 재설정했습니다.");
}
}
}

소스를 보면 point cut 에 annotation(transactional) 다음에  "if transactional.readOnly()" 를 추가하여,  @Transactional(readOnly=true) 로 지정된 메소드만 intercept 하도록 했습니다.
이렇게 하면 부가적인 인터셉트 과정을 거치지 않아서 좋겠지요.

두번째는 Hibernate Session#connection() 이 deprecated 된다고, doWork() 를 추천하더군요. 그래서 Connection을 readOnly 로 지정하는 Work와 복원하는 Work 를 구현하여 사용했습니다.

한가지 제가 아직 해결 못한게, Intercepting 하고자 하는 메소드가 Concrete class 만 가능하고, Interface는 안되는 군요... 이 것 때문에 Spring-Data-Jpa 의 JpaRepository 에 직접 @Transactional 을 할 수 없고, Business Logic 의 Service Component 에 Transactional 을 지정해 주셔야 합니다.

개인적으로 Hibernate 자체를 사용하는 것을 선호했지만, Spring-Data-Jpa 의 많은 장점을 보고, JPA 로 넘어가려고 합니다.

앞으로는 JPA 를 기준으로 개발할 거 같네요.

 ==================

 구글링을 계속 해보니, Spring 3.0 이후로는 인터페이스에 정의된 annotation 는 상속되지 않는다고 나왔네요... 쩝... 이게 자바의 규칙이라고 하네요^^

AspectJ follows Java's rule that annotations on interfaces are not inherited.

참고 :
    Aspect Oriented Programming with Spring 중에 7.8.2 Other Spring aspects for AspectJ

2013년 7월 20일 토요일

Spring-Data-Mongo 이용하기

NoSQL DB 중 범용성이 좋은 MongoDB 를 사용하려면 여러가지 방법이 있습니다

1. Hibernate-OGM for MongoDB
2. Spring-Data MongoDB 

1번은 hibernate-core, hibernate-search, hibernate-ogm 을 활용하여 검색 시스템을 만들어봐서, 이번에는  spring-data-mongodb 를 사용해보기로 했습니다.

예전에 spring-data-jpa 에서 repository 의 concrete class 를 안 만들고, 동적으로 생성해서 사용하는 방법을 보고, 와 정말 이렇게 되면 코딩량이 엄청 줄어들겠다 싶더군요...
그래서 이번에 spring-data-mongo 의 예제를 보고, 제 나름대로 다시 구성해 봤습니다.

spring-data 의 여러가지 모듈 사용법이 더 좋고, 생산성이 높다면, 앞으로는 이 것을 계속 사용하려고 합니다...

우선 spring-data-mongo 를 사용하기 위해  dependency에 다음을 추가합니다.

pom.xml

<!-- Spring framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<!-- Spring Data MongoDB -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
<!-- QueryDSL for MongoDB -->
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-mongodb</artifactId>
</dependency>
<!-- QueryDSL apt -->
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

다음으로 도메인 모델로 Album class 를 정의합니다.

Album.java
@Document
@Getter
@Setter
public class Album extends ValueObjectBase {
@Id
private ObjectId id;
private String title;
private String artist;
private List<Track> tracks = new ArrayList<Track>();
public Album(String title, String artist) {
Guard.shouldNotBeEmpty(title, "title");
Guard.shouldNotBeEmpty(artist, "artist");
this.title = title;
this.artist = artist;
}
public void add(Track tract) {
this.tracks.add(tract);
}
@Override
public int hashCode() {
return HashTool.compute(id);
}
@Override
public Objects.ToStringHelper buildStringHelper() {
return super.buildStringHelper()
.add("id", id)
.add("title", title)
.add("artist", artist);
}
private static final long serialVersionUID = 958798390838794475L;
}
view raw Album.java hosted with ❤ by GitHub
@Document 는 org.springframework.data.mongodb.core.mapping.Document 입니다.
이 어노테이션이 정의되면, MongoDB 의 Document 로 선언한 것입니다.

다음은 Album용 Repository를 정의합니다.

AlbumRepository.java
package kr.hconnect.mongo.test.music.model;
import org.bson.types.ObjectId;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Simple repositories interface to manage {@link Album} instances.
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 7. 20. 오후 4:16
*/
@Repository
public interface AlbumRepository extends CrudRepository<Album, ObjectId>, QueryDslPredicateExecutor<Album> {
List<Album> findByTracksName(String name);
List<Album> findByTracksNameLike(String name);
List<Album> findByTracksRatingGreaterThan(Stars rating);
}


테스트용 환경 설정은 다음과 같이 합니다.
MongoConfiguration.java
package kr.hconnect.mongo.test.music;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import kr.hconnect.mongo.test.music.model.AlbumRepository;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* kr.hconnect.mongo.test.music.MongoConfiguration
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 7. 20. 오후 4:34
*/
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
/** 이게 있어야 Repository 의 Concrete Class를 생성해줍니다.*/
@EnableMongoRepositories(basePackageClasses = { AlbumRepository.class }, repositoryImplementationPostfix = "Impl")
//@ComponentScan(basePackageClasses = { AlbumRepository.class })
public class MongoConfiguration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "musicDB";
}
@Override
public Mongo mongo() throws Exception {
return new MongoClient("localhost");
}
}

주의할 점은 Repository 들을 실제 구현한 것이 아니라, Spring 이 동적으로 구현할 수 있도록 @EnableMongoRepositories 를 선언해 줘야 한다는 것입니다.

마지막으로 테스트 코드는 다음과 같습니다.

AlbumRepositoryIntegrationTest.java
package kr.hconnect.mongo.test.music.repositories;
import kr.hconnect.mongo.test.music.AbstractIntegrationTest;
import kr.hconnect.mongo.test.music.MongoConfiguration;
import kr.hconnect.mongo.test.music.model.Album;
import kr.hconnect.mongo.test.music.model.AlbumRepository;
import kr.hconnect.mongo.test.music.model.Stars;
import org.hamcrest.CoreMatchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import static org.fest.assertions.Assertions.assertThat;
/**
* kr.hconnect.mongo.test.music.repositories.AlbumRepositoryIntegrationTest
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 7. 20. 오후 4:53
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MongoConfiguration.class })
public class AlbumRepositoryIntegrationTest extends AbstractIntegrationTest {
@Autowired
AlbumRepository repository;
@Before
public void purgeRepository() {
repository.deleteAll();
super.setup();
}
@Test
public void createAlbum() throws Exception {
repository.save(albums);
assertSingleGruxAlbum(repository.findOne(bigWhiskey.getId()));
}
@Test
public void findsAlbumByConcreteTrackName() throws Exception {
repository.save(albums);
assertSingleGruxAlbum(repository.findByTracksName("Grux"));
List<Album> albums = repository.findByTracksName("Foo");
assertThat(albums.isEmpty()).isTrue();
}
@Test
public void findsAllAlbumsByTrackNameLike() throws Exception {
repository.save(albums);
assertBothAlbums(repository.findByTracksNameLike("*it*"));
}
@Test
public void findsAlbumsByTrackRating() throws Exception {
bigWhiskey.getTracks().get(4).setRating(Stars.FOUR);
repository.save(albums);
assertSingleGruxAlbum(repository.findByTracksRatingGreaterThan(Stars.THREE));
List<Album> albums = repository.findByTracksRatingGreaterThan(Stars.FOUR);
assertThat(albums.isEmpty()).isTrue();
}
private void assertSingleGruxAlbum(List<Album> albums) {
Assert.assertThat(albums, CoreMatchers.is(CoreMatchers.notNullValue()));
Assert.assertThat(albums.size(), CoreMatchers.is(1));
Assert.assertThat(albums.get(0), CoreMatchers.is(CoreMatchers.notNullValue(Album.class)));
assertSingleGruxAlbum(albums.get(0));
}
}

를 구현하면 됩니다.
나머지 코드는  Spring-Data Mongo 의 예제에 있습니다. 그 예제는 xml 로 환경설정을 하고, 몇가지 제가 필요없는 코드를 제거했습니다.

시간나면 github에 따로 분리해서 올리도록 해보겠습니다...


Infinispan 캐시 저장소로 MongoDB 사용하기

분산캐시로 JBoss 에서 제작한 Infinispan 은 메모리 캐시로만 알고 있었는데, 여러가지 Cache Store 를 이미 제공하고 있더군요. 특히 Lucene 용이라던지, Jdbc 라던지...

Infinispan 5.3 부터는 MongoDB 를 캐시 저장소로 사용이 가능하더군요. 캐시 중에 영구 저장소에 저장해야 할 경우이면서, 빠른 PUT 성능이 필요로 하는 곳에 쓰면 좋을 것 같습니다.

우선 저는 Infinispan을 Hibernate 2nd cache 로 사용하기도 하는데요, RDBMS 보다야 캐시가 빠르지만, 보통은 휘발성이라 중간 접점이 있으면 좋겠다 싶었는데, Infinispan이 저장소로 NoSQL 을 사용할 수 있다면, 더 좋겠다 싶더군요.

제가 보통 이런 것의 사용처로는 통계성 데이터의 백데이터를 하루나 일주일 정도 캐시에 남겨 놓는 것입니다. 메모리가 제한적이니, NoSQL 에 저장했다가 거기서 로드해서 사용하고, RDBMS 는 처음 읽기만 하고 더 이상 접근하지 않는다면 좋겠다 싶어서지요.

그럼 Infinispan 과 MongoDB 를 활용해서 캐시를 사용해 보도록 하겠습니다.

<!-- Infinispan -->
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-spring</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-lucene-v3</artifactId>
<version>${infinispan.version}</version>
</dependency>
<!-- Infinispan cacheStore MongoDB -->
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-mongodb</artifactId>
<version>${infinispan.version}</version>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

우선 infinispan-core, infinispan-cachestore-mongodb 를 maven dependency 에 추가합니다. 현재 가장
최신 버전은 5.3.0.Final 입니다.

코드는 Infinispan을 사용하는 것과 같고, 다만 CacheStore를 설정해주는 부분만 새로 추가하면 됩니다.
여기서는 ConfigurationBuilder 를 이용하여 MongoDB CacheStore 설정을 수행했습니다.

MongoDbAsCaceStoreTest.java
@Test
@SuppressWarnings("unchecked")
public void configurationTest() throws InterruptedException {
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.loaders().addStore(MongoDBCacheStoreConfigurationBuilder.class)
.host("localhost")
.port(27017)
.timeout(2000)
.acknowledgment(0)
.database("infinispan_cachestore")
.collection("entries");
final Configuration config = builder.build(true);
MongoDBCacheStoreConfiguration store = (MongoDBCacheStoreConfiguration) config.loaders().cacheLoaders().get(0);
DefaultCacheManager manager = new DefaultCacheManager();
manager.defineConfiguration("entries", config);
Cache cache = manager.getCache("entry");
assertThat(cache).isNotNull();
for (int i = 0; i < 5000; i++) {
cache.put("Element-" + i, "Value-" + i);
}
Thread.sleep(100);
assertThat(cache.get("Element-0")).isEqualTo("Value-0");
manager.stop();
}

보안을 위해서 username, password 도 설정이 가능합니다.
좀 더 자세한 내용은 Infinispan CacheStore  를 참고하세요.

참 MongoDB 에 접속하기 위해 Java 용 Driver 도 dependency에 추가해 주세요.

2013년 7월 9일 화요일

IntelliJ 12 + lombok project plugin 이 동작하지 않을때...

IntelliJ 12용 lombok plugin 0.6.2 가 릴리즈되어 기쁜 마음으로 Upgrade 했습니다.
아니 근데.. 잘 되던 @Getter, @Setter 가 잘 안되네요...
쩝 뭐가 꼬였나... maven repository도 삭제해보고, pom 파일도 다시 import 해보고 테스트 해보니 maven으로는 제대로 작동하는데 IntelliJ 자체에서 작업 시 예외가 발생하더군요...

plugin 문제인가 싶어, rollback 하려다가 setting 창에 요런 게 새로 생겼네요 (그 전에도 있었나 모르겠지만, 전 지금 처음 알았습니다.)




아니 뻘건 줄로 뭔 말을 썼는데... external compiler option? annotation processors? 을 활성화 시키라구? external compiler option 은 scala 에서만 쓰는 줄 알았는데? annotation processor 는 lombok 에서 필요한 기능인 줄 알겠는데... 저게 어디 있는건데?...  창에서 검색할 수 있으니 좋네요^^ 


요 화면과 같이 Compiler 내에 Annotation Processors 라는 항목이 있군요. 창 최상단에 있는 Enable annotation processing 항목을 check 해주고, 제일 아래의 "Apply" 버튼을 누른 후, lombok plugin 설정 창으로 가보면


다음과 같이 바뀌어 있을 겁니다. 아니면 Verify xxxx 버튼을 누르면 lombok 을 제대로 사용할 수 있는지 다시 평가해 줍니다.

흠 버전 업하면서, 옵션으로 변경되었나 봅니다.
요것때문에 한 시간을 허비했네 쩝...





2013년 7월 8일 월요일

Scala 와 Java 의 자료구조 변환 문제를 쉽게 해결합시다.

Scala의 장점 중에 하나가 상당히 다양하고, 강력한 자료구조 및 편의성을 제공하는 것입니다. 특히 자료구조를 병렬로 처리하는 것도 아주 쉽게 적용할 수 있습니다.
이런 Scala의 장점을 Java에서 사용하기 위해서 단순한 작업 두 가지만 해주면, Scala 의 강력한 자료구조를 쉽게 사용할 수 있습니다.

첫째는 maven으로 java와 scala 코드를 동시에 빌드할 수 있는 환경을 만드는 것이고,
둘째는 Java 자료구조와 Scala 자료구조를 쉽게 변환할 수 있도록 하는 것입니다.

첫째는 링크를 따라가시면 쉽게 환경을 구성할 수 있으니 생략하구요,
자료구조의 변환에 대한 코드만 보여드리겠습니다.
물론 제가 만든 게 아니라 Scala 에서 기본 제공하는 JavaConversions 라는 Object 를 사용하는 것입니다.

Java2Scala Object 를 보시면 모든 메소드가 implicit 로 정의되어 있습니다. 암묵 변환(implicit conversion) 이 되서, 개발자는 Object 를 import 하는 것으로 끝납니다.

Java2Scala.scala

import collection.{mutable, JavaConversions}
/**
* Java 자료구조 (Iterable, Iterator, List, Set, Map)와 Scala 자료구조 (Iterable, Iterator, List, Set, Map) 를
* 암묵적(implicit)하게 변환해줍니다.
* 사용 시 import kr.hconnect.core.collections.Java2Scala._ 를 해주시면 됩니다.
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 6. 27. 오후 1:43
*/
object Java2Scala {
implicit
def javaIterable2ScalaIterable[T](iterable: java.lang.Iterable[T]): Iterable[T] =
JavaConversions.iterableAsScalaIterable[T](iterable)
implicit
def javaIterator2ScalaIterator[T](iterator: java.util.Iterator[T]): Iterator[T] =
JavaConversions.asScalaIterator(iterator)
implicit
def javaList2ScalaBuffer[T](list: java.util.List[T]): mutable.Buffer[T] =
JavaConversions.asScalaBuffer[T](list)
implicit
def javaSet2ScalaSet[T](set: java.util.Set[T]): mutable.Set[T] =
JavaConversions.asScalaSet[T](set)
implicit
def javaMap2ScalaMap[K, V](javaMap: java.util.Map[K, V]): mutable.Map[K, V] =
JavaConversions.mapAsScalaMap[K, V](javaMap)
implicit
def scalaIterable2JavaIterable[T](iterable: collection.Iterable[T]): java.lang.Iterable[T] =
JavaConversions.asJavaIterable[T](iterable)
implicit
def scalaIterator2JavaIterator[T](iterator: collection.Iterator[T]): java.util.Iterator[T] =
JavaConversions.asJavaIterator[T](iterator)
implicit
def scalaBuffer2JavaList[T](buffer: collection.mutable.Buffer[T]): java.util.List[T] =
JavaConversions.bufferAsJavaList[T](buffer)
implicit
def scalaBuffer2JavaList[T](seq: collection.Seq[T]): java.util.List[T] =
JavaConversions.seqAsJavaList[T](seq)
implicit
def scalaBuffer2JavaList[T](mutableSeq: collection.mutable.Seq[T]): java.util.List[T] =
JavaConversions.mutableSeqAsJavaList[T](mutableSeq)
implicit
def scalaSetToJavaSet[T](set: collection.Set[T]): java.util.Set[T] =
JavaConversions.setAsJavaSet(set)
implicit
def scalaBuffer2JavaList[T](mutableSet: collection.mutable.Set[T]): java.util.Set[T] =
JavaConversions.mutableSetAsJavaSet(mutableSet)
implicit
def scalaMapToJavaMap[K, V](map: collection.Map[K, V]): java.util.Map[K, V] =
JavaConversions.mapAsJavaMap[K, V](map)
}

java 코드에서 scala 메소드를 사용할 때, java 의 list 나 set 을 전달하면, 알아서 scala 의 ArrayBuffer 나 HashSet으로 변환하여 작업한 후 결과도 java 의 자료구조 형식으로 변환합니다.

scala 의 implicit 기능을 사용하면 엄청난 유연성이 생깁니다.


MySql Master Slave Replication 에서 Hibernate 사용하기 2

MySQL Master Slave Replication 에서 Hibernate 사용하기 에서 만든 ConnectionInterceptor 는 Custom Annotation을 사용하여, 부가적인 노가다가 필요했습니다.

보통 Spring 을 사용하니 Spring 의 @Transactional 을 이용하여, readOnly 속성에 따른 작업을 수행하면 될 것입니다.
제가 Spring Aop 에 초보라 좀 찾아보는데 시간이 걸리네요...

어쨌든 구글링을 통해 방법을 찾아서 개선된 ConnectionInterceptor 를 올립니다.

@Transactional을 사용하는 ConnectionInterceptor.java

/**
* MySQL Replication 환경 (Master-Slave)에서
* {@link org.springframework.transaction.annotation.Transactional#readOnly()} 이 true로 정의된 Method에 대해서는
* Slave 서버로 접속하기 위해, {@link java.sql.Connection#isReadOnly()}의 속성을 true로 변경하여 작업을 수행하도록 합니다.
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 7. 5. 오후 11:07
*/
@Aspect
@Component
public class ConnectionInterceptor {
private static final Logger log = LoggerFactory.getLogger(ConnectionInterceptor.class);
private static final boolean isTraceEnabled = log.isTraceEnabled();
@Autowired
private SessionFactory sessionFactory;
@Around(value = "@annotation(transactional)", argNames = "transactional")
public Object proceed(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable {
if (!transactional.readOnly()) {
return pjp.proceed();
} else {
log.debug("읽기전용 작업을 수행하기 위해 현 connection를 readonly로 설정합니다...");
SessionImpl session = (SessionImpl) sessionFactory.getCurrentSession();
Connection connection = session.connection();
boolean autoCommit = connection.getAutoCommit();
boolean readOnly = connection.isReadOnly();
try {
// MySQL SLAVE 서버에 접속하기 위해 Connection 속성을 설정합니다.
connection.setAutoCommit(false);
connection.setReadOnly(true);
// @ReadOnlyConnection이 선언된 메소드를 실행합니다.
return pjp.proceed();
} finally {
connection.setAutoCommit(autoCommit);
connection.setReadOnly(readOnly);
log.debug("읽기전용 작업을 수행하고, connection의 원래 설정으로 재설정했습니다.");
}
}
}
}

보시다시피 @Transactional 이 정의된 메소드에 readOnly=true 인 경우에는 Slave 에 접속하게하고, 아니라면 그대로 실행하도록 합니다.

2013년 7월 6일 토요일

MySQL Replication 환경 하에서 Hibernate 사용하기

타 RDBMS 보다 MySQL이 좋은 점 중에 하나가 다중 서버 환경으로의 전환이 용이하고, 안정적이라는 점이다. 보통의 Legacy 시스템에서는 RDBMS 서버 한대를 두고, 백업 서버를 두는 방식을 택하고, 성능이 모자라면 scale up 을 수행하는데, MySQL은 Replication을 이용하여 scale out 을 수행하므로서 부하 분산으로 성능을 높히는 방식입니다.



Hibernate의 경우 SessionFactory가 RDBMS 와 1:1 매핑이 되는 관계라 위와 같이 멀티 서버의 경우에는 중간에 Proxy 서버를 두던가,  MySQL Replication Driver 를 이용해야 합니다.
Proxy 서버를 둔다는 것은 Clustering 을 구성하는 것과 같으니 Proxy 서버 주소만 알면 되지만, Replication Driver를 사용 시에는 Master / Slave 서버별로 작업의 특성에 따라 구분해 줘야 합니다.

즉 Master 서버는 Read/Write를 할 수 있지만, Slave 서버는 Read 작업만 수행해야 합니다. 이렇게 하려면 작업 전에 Connection 의 isReadOnly 속성을 변경하여, 원하는 종류의 서버를 선택하게끔 해야 합니다.

이를 위해 Spring AOP 를 이용하여, 작업 시작 전에 작업 종류에 따라 Connection 속성을 변경하는 interceptor 를 제작합니다.

ConnectionInterceptor.java
@Aspect
@Component
public class ConnectionInterceptor {
private static final Logger log = LoggerFactory.getLogger(ConnectionInterceptor.class);
private static final boolean isTraceEnabled = log.isTraceEnabled();
@Autowired
private SessionFactory sessionFactory;
@Around("@annotation(kr.hconnect.data.mysql.ReadOnlyConnection)")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
log.debug("읽기전용 작업을 수행하기 위해 현 connection를 readonly로 설정합니다...");
SessionImpl session = (SessionImpl) sessionFactory.getCurrentSession();
Connection connection = session.connection();
boolean autoCommit = connection.getAutoCommit();
boolean readOnly = connection.isReadOnly();
try {
// MySQL SLAVE 서버에 접속하기 위해 Connection 속성을 설정합니다.
connection.setAutoCommit(false);
connection.setReadOnly(true);
// @ReadOnlyConnection이 선언된 메소드를 실행합니다.
return pjp.proceed();
} finally {
connection.setAutoCommit(autoCommit);
connection.setReadOnly(readOnly);
log.debug("읽기전용 작업을 수행하고, connection의 원래 설정으로 재설정했습니다.");
}
}
}

line 11 에 있는 @Arount 를 보시면,  ReadOnlyConnection 이라는 annotation 이 있는 메소드를 intercept 하도록 합니다.  이 메소드는 readonly 작업을 뜻하므로 connection의 readonly 값을 true 로 하여, MySQL 의 Slave 서버에 접속하도록합니다.

ReadOnlyConnection annotation은 메소드에만 적용되도록 합니다.

ReadOnlyConnection.java
@Target( { ElementType.METHOD } )
@Retention( RetentionPolicy.RUNTIME )
@Inherited
public @interface ReadOnlyConnection {
}

자 이제 테스트를 위한 서비스를 제작해 봅시다.

SimpleEntityServiceImpl.java
@Slf4j
@Service
@SuppressWarnings("unchecked")
public class SimpleEntityServiceImpl implements SimpleEntityService {
@Autowired SessionFactory sessionFactory;
@Transactional( readOnly = true )
@ReadOnlyConnection
@Override
public List<SimpleEntity> findAll() {
Session session = sessionFactory.getCurrentSession();
return (List<SimpleEntity>)session.createCriteria(SimpleEntity.class).list();
}
}
서비스 클래스에서 조회 메소드에서 @ReadOnlyConnection 을 사용하므로서 AOP 를 통해 ConnectionInterceptor 를 통해, Slave 서버에 접속하도록 했습니다.

이를 통해 Master/Slave 를 구분하고,  여러대의 Slave 도 Replication Driver 가 RoundRobin 방식으로  서버를 지정해주니, Application 개발자는 RDBMS 환경에 크게 신경 쓰지 않고, 자신의 분야에만 집중 할 수 있게 될 겁니다^^

또 한가지 SessionFactory가 하나로 유지되므로, 2nd Cache 도 하나가 되므로, 성능상의 잇점과 Cache와의 불일치에 대해 다른 방식보다 어느 정도 잇점이 있을 것입니다