티스토리 뷰

목표

  • MySQL 또는 MariaDB의 레플리케이션(1개 마스터, n개 슬레이브)를 지원하는 하나의 DataSource 빈을 생성한다.

  • 앞서 생성한 DataSource 빈을 관리하는 PlatformTransactionManager 빈을 생성한다.

  • 앞서 생성한 DataSource 빈에 연결하는 MyBatisSqlSession 빈을 생성한다.

기대효과

  • 개발자가 직접 각 데이터베이스를 직접 바라보는 로우 레벨의 DataSource 구성을 관리할 필요 없이 평소와 동일하게 비즈니스 로직에 집중할 수 있다.

  • @Service 클래스의 각 메써드 레벨에 명시하는 @Transactional 어노테이션으로 마스터, 슬레이브로 향할 SQL 문을 결정할 수 있다. @Transactional(readOnly = true)이면 슬레이브로, @Transactional(readOnly = false)이면 마스터로 쿼리를 실행한다.

사전지식

application.yml

  • /src/main/resources/application.yml 파일에 아래 내용을 추가한다. 예시일 뿐 운영 환경의 자세한 설정은 관련 링크를 참고한다. [관련 링크1] [관련 링크2]

spring:
master:
    datasource:
        type: org.apache.tomcat.jdbc.pool.DataSource
        driverClassName: net.sf.log4jdbc.DriverSpy
        url: jdbc:log4jdbc:mariadb://your-master-db-domain:3306/your-db
        username: username
        password: password
        testOnBorrow: true
        validationInterval: 34000
        validationQuery: SELECT 1
slave:
    datasource:
        type: org.apache.tomcat.jdbc.pool.DataSource
        driverClassName: net.sf.log4jdbc.DriverSpy
        url: jdbc:log4jdbc:mariadb://your-slave-db-domain:3306/your-db
        username: username
        password: password
        testOnBorrow: true
        validationInterval: 34000
        validationQuery: SELECT 1
  • testOnBorrow: 운영 환경에서 필수 설정이다. 기본 값은 falsetrue로 설정하면 커넥션 풀에서 커넥션을 빌릴 때 마다 유효한 커넥션인지 검사한다. 끊긴 커넥션일 경우 커넥션 풀에서 해당 커넥션을 버리도록 한다. 이 옵션은 혼자 만으로는 작동할 수 없으며 아래 설명할 validationInterval, validationQuery 설정을 필요로 한다.


  • validationInterval: 앞서 커넥션 풀에서 빌리는 최초 시점에만 유효한 커넥션인지 검사하는 것보다 훨씬 적극적으로 설정된 시간마다 유효 커넥션 여부를 검사한다. 기본 값은 3000(3초)로 운영 환경에서는 34000(34초)을 권장한다. MySQL(또는 MariaDB) 상에서는 반대로 wait_timeout(SELECT @@wait_timeout) 값에 정의된 시간만큼 사용되지 않는 커넥션을 해제하므로 실제 데이터베이스 설정 값을 고려하여 설정해야 한다. 당연히 주기가 짧아질수록 빈번한 조회로 성능은 하락한다.

  • validationQuery: 유효 커넥션 여부를 실제 검증하는 쿼리문을 작성한다. 가장 부하가 적은 SELECT 1로 설정한다.

DataSourceConfig

  • 아래와 같이 @Configuration 클래스를 작성하면 모든 준비가 완료된다. 기본 프로젝트 구성은 이 글을 참고한다.
package com.jsonobject.example.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mybatis.replication.datasource.ReplicationRoutingDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Primary
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.master.datasource")
    public DataSource masterDataSource() {

        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.slave.datasource")
    public DataSource slaveDataSource() {

        return DataSourceBuilder.create().build();
    }

    @Bean(name = "routingDataSource")
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {

        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(masterDataSource, null);
        routingDataSource.addSlave(slaveDataSource);

        return routingDataSource;
    }

    @Bean(name = "dataSource")
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {

        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {

        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, ApplicationContext applicationContext) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mapper/*.xml"));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {

        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

참고 글

댓글
댓글쓰기 폼