Article Image
Article Image
read

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

Multi-Datasource

이런 경우 스프링에서 제공하는 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>

Gist - applicationContext.xml

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

RoutingDataSource.java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* @author sidnancy
*/

public class RoutingDataSource extends AbstractRoutingDataSource {
  @Override
  protected Object determineCurrentLookupKey() {
    return ContextHolder.getDataSourceType();
  }
}

Gist - RoutingDataSource.java

ContextHolder.java

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();
  }

}

Gist - ContextHolder.java

DataSourceType.java

package kr.sidnancy.annotation;

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)
public @interface DataSource {
    kr.sidnancy.common.type.DataSourceType value() default kr.sidnancy.common.type.DataSourceType.SLAVE;
}
package kr.sidnancy.common.type;

public enum DataSourceType {
  MASTER,SLAVE,DIST_READ,DIST_WRITE
}

Gist - DataSource

applicationContext.xml

그 후 DataSource가 RoutingDataSource에서 판단되는 값에 따라 설정될 수 있도록 아래와 같이 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>
 

Gist - applicationContext.xml

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

먼저 AOP 판단을 위한 class를 하나 만든다.

ExecutionLoggingAspect.java

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
@Component
@Order(value=1)
public class ExecutionLoggingAspect implements InitializingBean {

  private Logger log = Logger.getLogger(this.getClass());
  
  @Around(execution(* kr.sidnancy..*Service.*(..)))
  public Object doServiceProfiling(ProceedingJoinPoint joinPoint) throws Throwable {
  
    log.debug(@Service 시작”);
    
    //Annotation을 읽어 들이기 위해 현재의 method를 읽어 들인다.
    final String methodName = joinPoint.getSignature().getName();
    final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    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());
  
  Object returnValue = joinPoint.proceed();
  ContextHolder.clearDataSourceType();
  
  log.debug(@Service 끝”);
  
  return returnValue;
  
  }
}

Gist - ExecutionLoggingAspect.java

그리고 다시 applicationContext에 AOP 설정을 추가한다.

applicationContext.xml

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

Gist - applicationContext.xml

위와 같이 설정 후에 아래와 같은 @Service를 만들어 테스트를 해보면 결과가 잘 나온다.

Test Code

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)
  public List listUserReview(String userid) throws DataAccessException {
    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

Gist - Test Code

Blog Logo

Joseph Yoon


Published

Image

엔지니어와 아티스트 사이

엔지니어와 아티스트 사이 예술과 공학 사이

Back to Overview