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

댓글 없음: