SpringBoot Master/Slave DB 세팅
기존에 DB 를 하나만 사용하던 스프링 부트(버전 2.7.12) 프로젝트가 있다. 이 어플리케이션의 DB 세팅은 아래와 같이 application.properties 에 하나의 DB만 세팅 되어 있다.
spring.datasource.url=IP
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.username=계정
spring.datasource.password=비밀번호
spring.datasource.hikari.pool-name=풀이름
기타등등 hikari connection pool 세팅
이렇게 세팅을 해주면 스프링에서 알아서 데이터 소스를 만들어주고 사용자는 비즈니스 로직만 작성하면 된다. 하지만 두개의 DB 를 사용해야 한다면 개발자가 직접 DataSource 를 생성하는 코드를 작성해야 한다. 또한 기존의 코드도 수정해야 할 일이 생길 수 있다. 두개의 DataSource 중에 어떤 DataSource 를 선택해야 하는지를 결정해줘야 하기 때문이다.
지금 프로젝트는 아래와 같은 규칙을 가지고 있다.
- DB 1개 사용
- READ 기능 : @Transactional(readOnly=true) 사용
- CREATE, UPDATE, DELETE 기능 : @Transactional 사용
이 프로젝트의 DB 세팅을 두개로 변경해야 한다. CUD 를 수행하는 Master db와 READ 만 수행하는 Slave db 로 구성하기로 했고, 기존에 존재하던 기능의 코드 수정은 없이 변경된 DB 세팅만 적용하고 싶다. Transactional 의 readOnly 값이 true 이면 Slave DB 를 사용하고 그 외의 @Transactional 에서는 Master DB 를 사용하고, 이러한 일을 하기 위해 RoutingDataSource 를 사용한다.
application.properties 수정
일단 아래와 같이 application.properties 를 수정했다.
db.master.datasource.url=MASTER DB IP:PORT
db.master.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.master.datasource.username=계정
db.master.datasource.password=비밀번호
db.master.datasource.hikari.pool-name=master-db-pool
기타등등 hikari connection pool 세팅
db.slave.datasource.url=SLAVE DB IP:PORT
db.slave.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.slave.datasource.username=계정
db.slave.datasource.password=비밀번호
db.slave.datasource.hikari.pool-name=slave-db-pool
기타등등 hikari connection pool 세팅
사용할 DB 정보를 입력한다.
DataSource 생성
@Bean
@ConfigurationProperties(prefix = "db.master.datasource")
public DataSourceProperties dbMasterDataSourceProp() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "db.master.datasource.hikari")
public DataSource dbMasterDataSource() {
return dbMasterDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "db.slave.datasource")
public DataSourceProperties dbSlaveDataSourceProp() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "db.slave.datasource.hikari")
public DataSource dbSlaveDataSource() {
return dbSlaveDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
RoutingDataSource 를 만들기 전에 application.properties 의 DB 정보를 읽어 DataSource 를 만들어준다.
DataSourceRouter 구현
RoutingDataSource 스프링 빈을 만들기 전에 AbstractRoutingDataSource 를 상속받은 구현체가 필요하다.
public class RoutingDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
System.out.println("================== determineCurrentLookupKey: " + "slave");
return "slave";
} else {
System.out.println("================== determineCurrentLookupKey: " + "master");
return "master";
}
// 아래와 같은 코드로 충분하나 확인을 위해 위와 같이 코딩을 했다.
//return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
}
}
AbstractRoutingDataSource 를 상속받고 determineCurrentLookupKey 를 오버라이딩 해야 한다. 이 메서드는 DataSource 를 고르기 위한 Key 를 제공하는 역할을 한다. 위 예시 코드에서는 트랜잭션에 readOnly 가 있으면 slave, 아니면 master 라는 문자열을 반환한다.
RoutingDataSource 스프링 빈 생성
@Bean
@Primary // 위에서 생성한 DataSource 빈이 있기 때문에 primary 를 붙여줘야 한다.
public DataSource routingMasterSlaveDataSource() {
DataSource masterDataSource = dbMasterDataSource(); // 위에서 만든 Master DataSource
DataSource slaveDataSource = dbSlaveDataSource(); // 위에서 만든 Slave DataSource
RoutingDataSourceRouter routingDataSourceRouter = new RoutingDataSourceRouter();
Map<Object, Object> datasourceMap = new HashMap<>() {
{
put("master", masterDataSource);
put("slave", slaveDataSource);
}
};
routingDataSourceRouter.setTargetDataSources(datasourceMap);
routingDataSourceRouter.setDefaultTargetDataSource(masterDataSource);
return routingDataSourceRouter;
}
위에서 determineCurrentLookupKey 메서드를 통해 Key 를 얻을 수 있다고 했다. 이 Key 가 datasourceMap 을 조회하기 위해서 사용되는 것이다.
LazyConnectionDataSourceProxy 생성
RoutingDataSource 를 바로 사용하면 master, slave 라우팅이 안된다. 왜냐하면 스프링에서 트랜잭션을 시작할 때 트랜잭션 동기화 이전에 RoutingDataSource 빈에 디폴트로 지정해둔 master DataSource 를 통해 Connection 을 얻기 때문이다. 트랜잭션 동기화 이후에 DataSource 를 정하고 Connection 을 가져오게 하기 위해서 LazyConnectionDataSourceProxy 로 RoutingDataSource 를 감싸줘야 한다. 그러면 트랜잭션 동기화 이전에 Connection Proxy를 획득하고 이후에 readOnly 상태에 따라 DataSource 를 정하게 만들 수 있다.
@DependsOn("routingMasterSlaveDataSource") // routingDataSource 가 먼저 있어야 함
@Bean
public LazyConnectionDataSourceProxy lazyConnectionDataSource(
@Qualifier("routingMasterSlaveDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
lazyConnectionDataSource 빈을 만들기 위해선 routingMasterSlaveDataSource 가 만들어져 있어야 하기 때문에 @DependsOn 어노테이션을 사용했다.
EntityManagerFactory 생성
@Bean(name = "dbEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean dbEntityManagerFactory(@Qualifier("lazyConnectionDataSource") DataSource dataSource) {
HibernateJpaVendorAdapter vendor = new HibernateJpaVendorAdapter();
vendor.setGenerateDdl(false);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendor);
factory.setDataSource(dataSource);
factory.setJpaDialect(new HibernateJpaDialect());
factory.setPersistenceUnitName("db-em");
Map<String, String> map = new HashMap<>();
map.put("hibernate.show_sql ", env.getProperty("db.jpa.show-sql"));
factory.setJpaPropertyMap(map);
return factory;
}
LazyConnectionDataSource 를 사용하는 EntityManagerFactory 를 만든다. 필요한 하이버네이트 세팅도 여기서 추가해주면 된다.
TransactionManager 생성
@Bean(name = "dbTransactionManager")
public PlatformTransactionManager dbTransactionManager(@Qualifier("dbEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
jpaTransactionManager.setPersistenceUnitName("dbTransactionManager");
return jpaTransactionManager;
}
위에서 만든 EntityManagerFactory 를 통해 트랜잭션 매니저를 만들게 설정을 한다.
이제 MasterDB/SlaveDB 를 용도에 맞게 사용할 수 있는 설정이 마무리되었다. 이제 @Transactional(readOnly=true) 를 호출하면 determineCurrentLookupKey() 메서드에 있는 slave 로그가 찍히고, readOnly가 false 가 아닌 @Transaction 을 호출하면 master 로그가 찍히는 것을 확인할 수 있다.
세팅이 실제로 어떻게 동작하는지도 알아보겠습니다.
링크 : 글 작성중