2014. 8. 13. 15:54ㆍJava
원문 : http://sidnancy.kr/archives/216
Spring + MyBatis 사용을 할 때 아래와 같이 복수의 DB에 접근해야 되는 경우가 있을 수 있다.
이런 경우 스프링에서 제공하는 IsolationLevelDataSourceRouter을 사용할 경우 해결이 되겠지만 이 경우 Datasource 설정을 Dao별로 분리해야 하고, Class 단위 별로 설정해야 되므로 Read-Write Logic을 분리해야 해서 약간 불편한 점이 있다.
그래서 annotation 을 사용하여 method별로 datasource를 분리할 수 있는 방법을 고민하다가 AbstractRoutingDataSource을 상속받아 직접 구현하는 방식을 택하게 되었다.
먼저 아래와 같은 DataSource 설정이 있다고 가정한다.
applicationContext.xml
- <!-- Oracle DB Data Source-->
- <bean id="primaryDataSource" class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close" p:driverClassName="${primaryora.jdbc.driverClassName}"
- p:url="${primaryora.jdbc.url}" p:username="${primaryora.jdbc.username}"
- p:password="${primaryora.jdbc.password}" p:maxActive="${primaryora.jdbc.maxActive}" />
- <bean id="standbyDataSource" class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close" p:driverClassName="${standbyora.jdbc.driverClassName}"
- p:url="${standbyora.jdbc.url}" p:username="${standbyora.jdbc.username}"
- p:password="${standbyora.jdbc.password}" p:maxActive="${standbyora.jdbc.maxActive}" />
- <!-- MySQL Review Data Source -->
- <bean id="distReadDataSource" class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close" p:driverClassName="${dist.read.jdbc.driverClassName}"
- p:url="${dist.read.jdbc.url}" p:username="${dist.read.jdbc.username}"
- p:password="${dist.read.jdbc.password}" p:maxActive="${dist.read.jdbc.maxActive}" />
- <bean id="distWriteDataSource" class="org.apache.commons.dbcp.BasicDataSource"
- destroy-method="close" p:driverClassName="${dist.write.jdbc.driverClassName}"
- p:url="${dist.write.jdbc.url}" p:username="${dist.write.jdbc.username}"
- p:password="${dist.write.jdbc.password}" p:maxActive="${dist.write.jdbc.maxActive}" />
- <!-- Transaction Manager -->
- <bean id="transactionManager"
- class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref="dataSource" />
- </bean>
- <!-- define the SqlSessionFactory -->
- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
- <property name="dataSource" ref="dataSource" />
- <property name="typeAliasesPackage" value="kr.sidnancy.entity" />
- </bean>
먼저 Datasource를 Routing 할 수 있게 AbstractRoutingDataSource를 상속 받는 Class를 하나 만들고, 이어 ThreadLocal을 사용 하여 현재 Datasource를 판단할 수 있는 contextHolder Class를 하나 더 만들어 준다.
RoutingDataSource
- package kr.sidnancy.common.inf.datasource;
- import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
- /**
- * @author sidnancy
- */
- public class RoutingDataSource extends AbstractRoutingDataSource {
- @Override
- return ContextHolder.getDataSourceType();
- }
- }
ContextHolder.java
- package kr.sidnancy.common.inf.datasource;
- import kr.sidnancy.common.type.DataSourceType;
- /**
- * @author sidnancy81
- */
- public class ContextHolder {
- private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<DataSourceType>();
- public static void setDataSourceType(DataSourceType dataSourceType){
- contextHolder.set(dataSourceType);
- }
- public static DataSourceType getDataSourceType(){
- return contextHolder.get();
- }
- public static void clearDataSourceType(){
- contextHolder.remove();
- }
- }
DataSourceType
- package kr.sidnancy.common.type;
- public enum DataSourceType {
- MASTER,SLAVE,DIST_READ,DIST_WRITE
- }
applicationContext.xml
- 그 후 아래와 같이 context 설정을 해준다.
- <bean id="dataSource" class="kr.sidnancy.common.inf.datasource.RoutingDataSource">
- <property name="targetDataSources">
- <map key-type="kr.sidnancy.common.type.DataSourceType">
- <entry key="MASTER" value-ref="primaryDataSource" />
- <entry key="SLAVE" value-ref="standbyDataSource" />
- <entry key="DIST_READ" value-ref="distReadDataSource"/>
- <entry key="DIST_WRITE" value-ref="distWriteDataSource"/>
- </map>
- </property>
- <!-- Default DataSource -->
- <property name="defaultTargetDataSource" ref="standbyDataSource" />
- </bean>
이제 위의 설정을 실제 적용할 수 있는 AOP 설정이 필요하다. 필자는 @Service 단에서 Datasource를 판단할 수 있게 하였다.
먼저 AOP 판단을 위한 class를 하나 만든다.
ExecutionLoggingAspect.java
- package kr.sidnancy.common.inf.aop;
- import java.lang.reflect.Method;
- import java.util.Calendar;
- import java.util.Collection;
- import java.util.Iterator;
- import kr.sidnancy.annotation.DataSource;
- import kr.sidnancy.common.inf.datasource.ContextHolder;
- import kr.sidnancy.common.type.DataSourceType;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.beans.factory.InitializingBean;
- import org.springframework.core.annotation.Order;
- import org.springframework.stereotype.Component;
- import org.springframework.util.StopWatch;
- /**
- * Order를 주는 이유는 다른 기타의 AOP 설정보다 DataSource 설정이 먼저 들어가게 하기 위해서이다.
- *
- * @author sidnancy81
- *
- */
- @Aspect
- @Order(value=1)
- public class ExecutionLoggingAspect implements InitializingBean {
- private Logger log = Logger.getLogger(this.getClass());
- @Around("execution(* kr.sidnancy..*Service.*(..))")
- log.debug("@Service 시작");
- //Annotation을 읽어 들이기 위해 현재의 method를 읽어 들인다.
- final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- if(method.getDeclaringClass().isInterface()){
- method = joinPoint.getTarget().getClass().getDeclaredMethod(methodName, method.getParameterTypes());
- }
- //Annotation을 가져온다.
- DataSource dataSource = (DataSource) method.getAnnotation(DataSource.class);
- if(dataSource != null){
- //Method에 해당 dataSource관련 설정이 있을 경우 해당 dataSource의 value를 읽어 들인다.
- ContextHolder.setDataSourceType(dataSource.value ());
- }else{
- //따로 annotation으로 datasource를 지정하지 않은 경우에는 메소드 이름으로 판단
- //get*, select* 의 경우는 default, 그 외의 경우에는 MASTER
- if(!(method.getName().startsWith("get") || method.getName().startsWith("select"))){
- ContextHolder.setDataSourceType(DataSourceType.MASTER);
- }
- }
- log.debug("DataSource ===> " + ContextHolder.getDataSourceType());
- ContextHolder.clearDataSourceType();
- log.debug("@Service 끝");
- return returnValue;
- }
- }
그리고 다시 applicationContext에 AOP 설정을 추가한다.
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
public DataSourceType value();
}
applicationContext.xml
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <!-- @Service단에서 Transaction 처리도 함께 해주기 위해 transaction manager의 order는 2로 내려준다. -->
- <tx:annotation-driven proxy-target-class="true" order="2"/>
위와 같이 설정 후에 아래와 같은 @Service를 만들어 테스트를 해보면
- package kr.sidnancy.service.review.impl;
- import java.util.List;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.dao.DataAccessException;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import kr.sidnancy.common.annotation.DataSource;
- import kr.sidnancy.common.type.DataSourceType;
- import kr.sidnancy.condition.review.UserReviewCond;
- import kr.sidnancy.entity.review.UserReview;
- import kr.sidnancy.persistence.review.UserReviewMapper;
- import kr.sidnancy.service.review.IUserReviewService;
- /**
- * @author sidnancy
- *
- */
- @Service
- public class UserReviewService implements IUserReviewService {
- @Autowired
- private UserReviewMapper userReviewMapper;
- @Override
- @Transactional(readOnly=true)
- @DataSource(DataSourceType.DIST_READ)
- return userReviewMapper.listUserReview(userid);
- }
- }
아래와 같은 결과를 얻을 수 있다.
- 2012-03-14 17:27:24 DEBUG [PROFILE:57] - +-->[SERVICE_S] UserReviewService.getTotalCountOfUserReview()
- 2012-03-14 17:27:24 DEBUG [PROFILE:77] - DataSource ===> DIST_READ