Spring RoutingDataSource 성능 테스트

최준호·2022년 10월 10일
0

Spring

목록 보기
40/49
post-thumbnail

😂 어떤걸 성능 테스트하겠다고?

MySql의 Master + Slave 구조에서 Spring의 DataSource를 선택하기 위해 Spring 자체에서 제공하는 RoutingDataSource와 각 Master의 DataSource와 Slave DataSource를 작성하는 방법 2개 중 어떤 것이 성능이 괜찮을지 테스트하게 되었다.

당연히 나는 이 결과가 별로 차이가 없기를 바랬다. 더 쉬운게 성능까지 더 좋다면 더 좋은거고! 근데 그러기는 쉽지 않으니까!

📗 테스트 시작

✍ MySql

📄 MySql 세팅

Spring Boot + MySql(Master + Slave) 구성 (Feat. Mybatis) 이전 글에 docker를 사용하여 로컬에 세팅하는 방법을 통해 MySql을 세팅해두자.

✍ Routing

📄 RoutingDataSource 세팅

@Configuration
@MapperScan(value="com.example.project.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
    public static final String MASTER = "masterDataSource";
    public static final String SLAVE = "slaveDataSource";

    @Bean
    @ConfigurationProperties(prefix = "spring.master.datasource")
    public DataSource masterDataSource(){
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.slave.datasource")
    public DataSource slaveDataSource(){
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    //@Bean
    @Profile(value = "local")
    public DataSourceInitializer dataSourceInitializer(@Qualifier(MASTER) DataSource master, ApplicationContext applicationContext){
        ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
        resourceDatabasePopulator.addScript(applicationContext.getResource("classpath:db/h2/schema.sql"));
//        resourceDatabasePopulator.addScript(applicationContext.getResource("classpath:db/h2/data.sql"));

        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(master);
        dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
        return dataSourceInitializer;
    }

    @Bean
    @DependsOn({MASTER, SLAVE})
    public RoutingDataSource routingDataSource(@Qualifier(MASTER) DataSource master, @Qualifier(SLAVE) DataSource slave){
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(MASTER, master);
        targetDataSources.put(SLAVE, slave);
        
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(master);   //기본은 master
        
        return routingDataSource;
    }

    @Bean
    public LazyConnectionDataSourceProxy lazyDataSource(RoutingDataSource routingDataSource){
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(LazyConnectionDataSourceProxy routingDataSource){
        return new DataSourceTransactionManager(routingDataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(LazyConnectionDataSourceProxy dataSource, ApplicationContext applicationContext) throws Exception{
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
        sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
        return sessionFactory.getObject();
    }

}
@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
        String routing = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceConfig.SLAVE : DataSourceConfig.MASTER;
        log.debug("routing ? {}", routing);
        return routing;
    }
    
}

📄 RoutingMapper

@Mapper
public interface TesttMapper {
    //select
    void sel(int i);
    //insert
    void ins(String str);
    //delete
    void del();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.mapper.member.TesttMapper">
    <insert id="ins" parameterType="String">
        insert into testt (name) values (#{name})
    </insert>

    <select id="sel" parameterType="Integer" resultType="String">
        select `name` from testt where id = #{id}
    </select>

    <delete id="del">
        delete from testt
    </delete>
</mapper>

✍ 분리하여 세팅

📄 MasterDataSource 세팅

@Configuration
@MapperScan(value="com.example.project.test.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
@EnableTransactionManagement
public class MasterConfig {
    public static final String MASTER = "masterDataSource";

    @Bean("masterTest")
    @ConfigurationProperties(prefix = "spring.master.datasource1")
    public DataSource masterDataSource(){
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean("masterSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterTest") DataSource dataSource, ApplicationContext applicationContext) throws Exception{
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
        sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
        return sessionFactory.getObject();
    }

}

📄 SlaveDataSource 세팅

@Configuration
@MapperScan(value="com.example.project.test.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory")
@EnableTransactionManagement
public class SlaveConfig {
    public static final String SLAVE = "slaveDataSource";

    @Bean("slaveTest")
    @ConfigurationProperties(prefix = "spring.slave.datasource1")
    public DataSource slaveDataSource(){
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }


    @Bean("slaveSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveTest") DataSource dataSource, ApplicationContext applicationContext) throws Exception{
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        sessionFactory.setMapperLocations(applicationContext.getResources("classpath:db/mapper/**/*.xml"));
        sessionFactory.setConfigLocation(applicationContext.getResource("classpath:db/mybatis-config.xml"));
        return sessionFactory.getObject();
    }

}

📄 MasterMapper

@Mapper
public interface Testt2MasterMapper {
    //insert
    void ins(String name);
    //delete
    void del();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.test.master.Testt2MasterMapper">
    <insert id="ins" parameterType="String">
        insert into testt2 (name) values (#{name})
    </insert>

    <delete id="del">
        delete from testt2
    </delete>
</mapper>

📄 SlaveMapper

@Mapper
public interface Testt2SlaveMapper {
    void sel(int i);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.test.slave.Testt2SlaveMapper">
    <select id="sel" parameterType="Integer" resultType="String">
        select `name` from testt2 where id = #{id}
    </select>
</mapper>

✍ 그 외 세팅

📄 Service

public interface TestService {

    ...
    
    /**
     * 테스트
     */
    void ins1(int i);
    void sel1(int i);
    void ins2(int i);
    void sel2(int i);
}
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class TestServiceImpl implements TestService{
    private final TesttMapper testtMapper;
    private final Testt2MasterMapper testt2MasterMapper;
    private final Testt2SlaveMapper testt2SlaveMapper;
    
    @Transactional
    @Override
    public void ins1(int i) {
        testtMapper.ins("test"+i);
    }

    @Override
    public void sel1(int i) {
        testtMapper.sel(i);
    }

    @Transactional
    @Override
    public void ins2(int i) {
        testt2MasterMapper.ins("2test"+i);
    }

    @Override
    public void sel2(int i) {
        testt2SlaveMapper.sel(i);
    }
}

✍ 테스트

📄 테스트 코드

@SpringBootTest
public class TestServiceTest {
    @Autowired
    private LoginService loginService;

    @Autowired
    private TesttMapper testtMapper;

    @Autowired
    private Testt2MasterMapper testt2MasterMapper;

    @Autowired
    private Testt2SlaveMapper testt2SlaveMapper;

    @Test
    @RepeatedTest(value = 10000)
    void insTest(RepetitionInfo info){
        loginService.ins1(info.getCurrentRepetition());
    }

    @Test
    @RepeatedTest(value = 10000)
    void selTest(RepetitionInfo info){
        loginService.sel1(info.getCurrentRepetition());
    }

    @Test
    @RepeatedTest(value = 10000)
    void insTest2(RepetitionInfo info){
        loginService.ins2(info.getCurrentRepetition());
    }

    @Test
    @RepeatedTest(value = 10000)
    void selTest2(RepetitionInfo info){
        loginService.sel2(info.getCurrentRepetition());
    }

}

📄 실제로 Master와 Slave가 잘 작동하는지 확인!

Spring Boot + MySql(Master + Slave) 구성 (Feat. Mybatis) 이전 글을 참고하여 cmd 창에서 확인해보면 된다.

이런식으로 write는 master로 read는 slave로 가는 것을 확인!

✅ 결과 확인

결과는 참혹하다... insert의 경우 거의 차이가 없지만 select의 경우 2배 이상 차이가 난다... 쿼리 상 readonly 옵션 값을 db에도 적용시키고 있어서 그런거 같은데... 이 점을 동일하게 만들어서 테스트해보고 싶다.

JPA를 쓸경우에는 RoutingDataSource가 좋을거 같지만 MyBatis를 쓸 경우 Mapper를 개발자가 직접 따로 작성할 수 있기 때문에 DataSource를 직접 주입해서 따로 쓰는게 맞는거 같다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글