Spring+MyBatis 의 DataSource Routing

2014. 8. 13. 15:54Java

원문 : http://sidnancy.kr/archives/216


Spring + MyBatis 사용을 할 때 아래와 같이 복수의 DB에 접근해야 되는 경우가 있을 수 있다.


 

이런 경우 스프링에서 제공하는 IsolationLevelDataSourceRouter을 사용할 경우 해결이 되겠지만 이 경우 Datasource 설정을 Dao별로 분리해야 하고, Class 단위 별로 설정해야 되므로 Read-Write Logic을 분리해야 해서 약간 불편한 점이 있다.

그래서 annotation 을 사용하여 method별로 datasource를 분리할 수 있는 방법을 고민하다가  AbstractRoutingDataSource을 상속받아 직접 구현하는 방식을 택하게 되었다.

 

먼저 아래와 같은 DataSource 설정이 있다고 가정한다.

applicationContext.xml

  1. <!-- Oracle DB Data Source-->
  2. <bean id="primaryDataSource" class="org.apache.commons.dbcp.BasicDataSource"
  3. destroy-method="close" p:driverClassName="${primaryora.jdbc.driverClassName}"
  4. p:url="${primaryora.jdbc.url}" p:username="${primaryora.jdbc.username}"
  5. p:password="${primaryora.jdbc.password}" p:maxActive="${primaryora.jdbc.maxActive}" />
  6. <bean id="standbyDataSource" class="org.apache.commons.dbcp.BasicDataSource"
  7. destroy-method="close" p:driverClassName="${standbyora.jdbc.driverClassName}"
  8. p:url="${standbyora.jdbc.url}" p:username="${standbyora.jdbc.username}"
  9. p:password="${standbyora.jdbc.password}" p:maxActive="${standbyora.jdbc.maxActive}" />
  10. <!-- MySQL Review Data Source -->
  11. <bean id="distReadDataSource" class="org.apache.commons.dbcp.BasicDataSource"
  12. destroy-method="close" p:driverClassName="${dist.read.jdbc.driverClassName}"
  13. p:url="${dist.read.jdbc.url}" p:username="${dist.read.jdbc.username}"
  14. p:password="${dist.read.jdbc.password}" p:maxActive="${dist.read.jdbc.maxActive}" />
  15. <bean id="distWriteDataSource" class="org.apache.commons.dbcp.BasicDataSource"
  16. destroy-method="close" p:driverClassName="${dist.write.jdbc.driverClassName}"
  17. p:url="${dist.write.jdbc.url}" p:username="${dist.write.jdbc.username}"
  18. p:password="${dist.write.jdbc.password}" p:maxActive="${dist.write.jdbc.maxActive}" />
  19. <!-- Transaction Manager -->
  20. <bean id="transactionManager"
  21. class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  22. <property name="dataSource" ref="dataSource" />
  23. </bean>
  24. <!-- define the SqlSessionFactory -->
  25. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  26. <property name="dataSource" ref="dataSource" />
  27. <property name="typeAliasesPackage" value="kr.sidnancy.entity" />
  28. </bean>

 

먼저 Datasource를 Routing 할 수 있게 AbstractRoutingDataSource를 상속 받는 Class를 하나 만들고, 이어 ThreadLocal을 사용 하여 현재 Datasource를 판단할 수 있는 contextHolder Class를 하나 더 만들어 준다.

RoutingDataSource

  1. package kr.sidnancy.common.inf.datasource;
  2. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  3. /**
  4. * @author sidnancy
  5. */
  6. public class RoutingDataSource extends AbstractRoutingDataSource {
  7. @Override
  8. protected Object determineCurrentLookupKey() {
  9. return ContextHolder.getDataSourceType();
  10. }
  11. }

 

ContextHolder.java

  1. package kr.sidnancy.common.inf.datasource;
  2. import kr.sidnancy.common.type.DataSourceType;
  3. /**
  4. * @author sidnancy81
  5. */
  6. public class ContextHolder {
  7. private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<DataSourceType>();
  8. public static void setDataSourceType(DataSourceType dataSourceType){
  9. contextHolder.set(dataSourceType);
  10. }
  11. public static DataSourceType getDataSourceType(){
  12. return contextHolder.get();
  13. }
  14. public static void clearDataSourceType(){
  15. contextHolder.remove();
  16. }
  17. }

DataSourceType

  1. package kr.sidnancy.common.type;
  2. public enum DataSourceType {
  3. MASTER,SLAVE,DIST_READ,DIST_WRITE
  4. }

 

applicationContext.xml

  1. 그 후 아래와 같이 context 설정을 해준다.
  2. <bean id="dataSource" class="kr.sidnancy.common.inf.datasource.RoutingDataSource">
  3. <property name="targetDataSources">
  4. <map key-type="kr.sidnancy.common.type.DataSourceType">
  5. <entry key="MASTER" value-ref="primaryDataSource" />
  6. <entry key="SLAVE" value-ref="standbyDataSource" />
  7. <entry key="DIST_READ" value-ref="distReadDataSource"/>
  8. <entry key="DIST_WRITE" value-ref="distWriteDataSource"/>
  9. </map>
  10. </property>
  11. <!-- Default DataSource -->
  12. <property name="defaultTargetDataSource" ref="standbyDataSource" />
  13. </bean>

이제 위의 설정을 실제 적용할 수 있는 AOP 설정이 필요하다. 필자는 @Service 단에서 Datasource를 판단할 수 있게 하였다.
먼저 AOP 판단을 위한 class를 하나 만든다.

ExecutionLoggingAspect.java

  1. package kr.sidnancy.common.inf.aop;
  2. import java.lang.reflect.Method;
  3. import java.util.Calendar;
  4. import java.util.Collection;
  5. import java.util.Iterator;
  6. import kr.sidnancy.annotation.DataSource;
  7. import kr.sidnancy.common.inf.datasource.ContextHolder;
  8. import kr.sidnancy.common.type.DataSourceType;
  9. import org.aspectj.lang.ProceedingJoinPoint;
  10. import org.aspectj.lang.annotation.Around;
  11. import org.aspectj.lang.annotation.Aspect;
  12. import org.aspectj.lang.reflect.MethodSignature;
  13. import org.springframework.beans.factory.InitializingBean;
  14. import org.springframework.core.annotation.Order;
  15. import org.springframework.stereotype.Component;
  16. import org.springframework.util.StopWatch;
  17. /**
  18. * Order를 주는 이유는 다른 기타의 AOP 설정보다 DataSource 설정이 먼저 들어가게 하기 위해서이다.
  19. *
  20. * @author sidnancy81
  21. *
  22. */
  23. @Aspect
  24. @Order(value=1)
  25. public class ExecutionLoggingAspect implements InitializingBean {
  26. private Logger log = Logger.getLogger(this.getClass());
  27. @Around("execution(* kr.sidnancy..*Service.*(..))")
  28. public Object doServiceProfiling(ProceedingJoinPoint joinPoint) throws Throwable {
  29. log.debug("@Service 시작");
  30. //Annotation을 읽어 들이기 위해 현재의 method를 읽어 들인다.
  31. final String methodName = joinPoint.getSignature().getName();
  32. final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  33. Method method = methodSignature.getMethod();
  34. if(method.getDeclaringClass().isInterface()){
  35. method = joinPoint.getTarget().getClass().getDeclaredMethod(methodName, method.getParameterTypes());
  36. }
  37. //Annotation을 가져온다.
  38. DataSource dataSource = (DataSource) method.getAnnotation(DataSource.class);
  39. if(dataSource != null){
  40. //Method에 해당 dataSource관련 설정이 있을 경우 해당 dataSource의 value를 읽어 들인다.
  41. ContextHolder.setDataSourceType(dataSource.value ());
  42. }else{
  43. //따로 annotation으로 datasource를 지정하지 않은 경우에는 메소드 이름으로 판단
  44. //get*, select* 의 경우는 default, 그 외의 경우에는 MASTER
  45. if(!(method.getName().startsWith("get") || method.getName().startsWith("select"))){
  46. ContextHolder.setDataSourceType(DataSourceType.MASTER);
  47. }
  48. }
  49. log.debug("DataSource ===> " + ContextHolder.getDataSourceType());
  50. Object returnValue = joinPoint.proceed();
  51. ContextHolder.clearDataSourceType();
  52. log.debug("@Service 끝");
  53. return returnValue;
  54. }
  55. }

그리고 다시 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

  1. <aop:aspectj-autoproxy proxy-target-class="true" />
  2. <!-- @Service단에서 Transaction 처리도 함께 해주기 위해 transaction manager의 order는 2로 내려준다. -->
  3. <tx:annotation-driven proxy-target-class="true" order="2"/>


위와 같이 설정 후에 아래와 같은 @Service를 만들어 테스트를 해보면

  1. package kr.sidnancy.service.review.impl;
  2. import java.util.List;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.dao.DataAccessException;
  5. import org.springframework.stereotype.Service;
  6. import org.springframework.transaction.annotation.Transactional;
  7. import kr.sidnancy.common.annotation.DataSource;
  8. import kr.sidnancy.common.type.DataSourceType;
  9. import kr.sidnancy.condition.review.UserReviewCond;
  10. import kr.sidnancy.entity.review.UserReview;
  11. import kr.sidnancy.persistence.review.UserReviewMapper;
  12. import kr.sidnancy.service.review.IUserReviewService;
  13. /**
  14. * @author sidnancy
  15. *
  16. */
  17. @Service
  18. public class UserReviewService implements IUserReviewService {
  19. @Autowired
  20. private UserReviewMapper userReviewMapper;
  21. @Override
  22. @Transactional(readOnly=true)
  23. @DataSource(DataSourceType.DIST_READ)
  24. public List listUserReview(String userid) throws DataAccessException {
  25. return userReviewMapper.listUserReview(userid);
  26. }
  27. }


아래와 같은 결과를 얻을 수 있다.

  1. 2012-03-14 17:27:24 DEBUG [PROFILE:57] - +-->[SERVICE_S] UserReviewService.getTotalCountOfUserReview()
  2. 2012-03-14 17:27:24 DEBUG [PROFILE:77] - DataSource ===> DIST_READ