주요글: 도커 시작하기
반응형

신림프로그래머 모임 후 몇몇 분과 차 마시면서 대화를 하다가, 한 분으로부터 다음과 같은 상황을 처리하기 위해 시도했던 방법을 듣게 되었다.

  • DB 구조의 변경 (클러스터에서 MySQL 리플리케이션으로)
    • 변경 기능은 마스터 DB 사용
    • 조회 기능은 슬레이브 DB 사용
  • 기존 코드를 최대한 바꾸지 않길 원함
    • DB 구조로 바뀌기 전에 이미 <mybatis:scan> 태그를 사용해서 DAO 인터페이스만 정의
    • 기존 서비스 클래스는 모두 DAO 사용
    • 같은 DAO를 사용하더라도 변경 기능 서비스 클래스에서 사용하는 DAO는 마스터 DB에 붙어야 하고, 읽기 기능 서비스 클래스에서 사용하는 DAO는 슬레이브 DB에 붙어야 함 

시도한 방법을 듣다보니 트랜잭션 처리나 동시성 접근 등에서 문제의 소지가 있을 것 같아, 다른 방법을 찾아봤는데 약간의 노력으로 위 상황을 해소할 수 있을 것 같아 정리해본다.


먼저 마스터DB와 슬레이브DB에 대한 트랜잭션 관리자, DataSource, SqlSessionFactoryBean을 설정한다.


<!-- master db -->

<bean id="masterTx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="masterDS" />

</bean>


<bean id="masterDS" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

    ...

</bean>


<bean id="masterSqlSF" class="org.mybatis.spring.SqlSessionFactoryBean">

    <property name="dataSource" ref="masterDS" />

    ...

</bean>


<!-- slave db -->

<bean id="slaveTx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="slaveDS" />

</bean>


<bean id="slaveDS" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

    ...

</bean>


<bean id="slaveSqlSF" class="org.mybatis.spring.SqlSessionFactoryBean">

    ...

</bean>


그 다음 <mybatis:scan>에서 사용할 BeanNameGenerator 구현 클래스를 마스터용과 슬레이브용으로 작성한다.


import org.springframework.beans.factory.config.BeanDefinition;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;

import org.springframework.beans.factory.support.BeanNameGenerator;

import org.springframework.util.StringUtils;


public class MasterBeanNameGenerator implements BeanNameGenerator {

    @Override

    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String fullClassName = definition.getBeanClassName();

        String simpleClassName = StringUtils.unqualify(fullClassName);

        return StringUtils.uncapitalize(simpleClassName) + "Master";

    }

}


public class SlaveBeanNameGenerator implements BeanNameGenerator {

    @Override

    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String fullClassName = definition.getBeanClassName();

        String simpleClassName = StringUtils.unqualify(fullClassName);

        return StringUtils.uncapitalize(simpleClassName) + "Slave";

    }

}


MasterBeanNameGenerator는 검색된 DAO 인터페이스가 "dao.MemberDao"일 경우, 자동 생성되는 빈 객체의 이름을 "memberDaoMaster"로 생성한다. 비슷하게 SlaveBeanNameGenerator는 클래스 이름 뒤에 'Slave'를 붙인다.


이제 마스터용 설정과 슬레이브용 빈과 클래스를 사용해서 <mybatis:scan> 태그를 설정한다.


<mybatis:scan base-package="dao" factory-ref="masterSqlSF" 

                   name-generator="mybatis.MasterBeanNameGenerator" />


<mybatis:scan base-package="dao" factory-ref="slaveSqlSF" 

                   name-generator="mybatis.SlaveBeanNameGenerator" />


위와 같이 설정하면 한 DAO 인터페이스에 대해 두 개의 빈 객체를 생성하게 된다. 예를 들어, 자동 검색된 인터페이스가 MemberDao일 경우, 마스터용 MyBatis SqlSession을 사용하는 DAO 객체는 'memberDaoMaster' 빈으로 생성되고 슬레이브용 MyBatis SqlSession을 사용하는 DAO 객체는 'memberDalSlave' 빈으로 생성된다.


이제 마스터를 사용해야 하는 서비스 객체와 슬레이브를 사용해야 하는 서비스 객체에 알맞은(즉, 타입은 같지만 사용하는 DB가 서로 다른) DAO 객체를 주입해준다.


<!-- master 사용 서비스 -->

<bean id="placeOrderService" class="service.PlaceOrderServiceImpl">

    <property name="itemDao" ref="itemDaoMaster" />

    <property name="paymentInfoDao" ref="paymentInfoDaoMaster" />

    <property name="purchaseOrderDao" ref="purchaseOrderDaoMaster" />

</bean>


<!-- slave 사용 서비스 -->

<bean id="itemListService" class="service.ItemListService">

    <property name="itemDao" ref="itemDaoSlave" />

</bean>


이제 거의 다 왔다. 남은 작업은 서비스 별로 다른 트랜잭션 관리자를 사용하도록 설정해주는 것이다.


public class PlaceOrderServiceImpl implements PlaceOrderService {

    // 마스터 용 DAO 주입

    private ItemDao itemDao;

    private PaymentInfoDao paymentInfoDao;

    private PurchaseOrderDao purchaseOrderDao;


    @Transactional("masterTx") // 마스터용 트랜잭션 관리자 사용

    @Override

    public PurchaseOrderResult order(PurchaseOrderRequest orderRequest) {

        ...

    }

    // set 메서드

}


public class ItemListService {

    private ItemDao itemDao; // 슬레이브용 DAO 주입


    @Transactional("slaveTx") // 슬레이브용 트랜잭션 관리자 사용

    public List<Item> getAll() {

        ...

    }

    ...

}


이제 끝났다. 위 설정을 통해 order() 메서드는 마스터용 트랜잭션 관리자를 통해서 트랜잭션을 생성하고, 마스터용 DAO를 사용해서 마스터 DB에 붙게된다. 비슷하게 getAll() 메서드는 슬레이브용 트랜잭션 관리자를 이용해서 트랜잭션을 생성하고 슬레이브용 DAO를 사용해서 슬레이브 DB에 붙게된다.


+ Recent posts