본문 바로가기
spring/spring jpa

[SpringBoot] JPA Master/Slave 구조 분기 처리

by moonsiri 2023. 8. 23.
728x90
반응형

서비스를 운영하다 보면 데이터베이스가 여러 개의 노드로 분산되어 Master/Slave 구조로 이루어져 있는 경우가 많습니다.

보통 두가지 방법으로 분기처리가 가능한데요. 본 포스팅에서는 @Transactional 어노테이션을 사용하는 방식을 소개해드리겠습니다.

 

 

먼저 Transactional readOnly에 따라 분기하는 CustomRoutingDataSource를 생성합니다.

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class RoutingDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "slave" : "master";
	}
}

 

다만 이 경우에는 트랜잭션이 시작하더라도 실제 쿼리 발생 직전에 connection을 생성해야 하기 때문에 AbstractRoutingDataSource를 상속받는 데이터 소스를 LazyConnectionDataSourceProxy로 감싸야합니다.

@Configuration
@EnableJpaRepositories(basePackages = {"com.moonsiri.**.repository"}, entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "jpaTxManager")
public class JpaConfiguration {

	@Bean("mainDatasource")
	public DataSource mainDatasource(@Qualifier("mainJdbcProperties") HikariConfig hikariConfig) {
		hikariConfig.setJdbcUrl(mainJdbcProperties.getUrl());
		hikariConfig.setUsername(mainJdbcProperties.getUsername());
		hikariConfig.setPassword(mainJdbcProperties.getPassword());
		hikariConfig.setDriverClassName(mainJdbcProperties.getDriverClassName());

		return new HikariDataSource(hikariConfig);
	}

	@Bean("readDatasource")
	public DataSource readDatasource(@Qualifier("readJdbcProperties") HikariConfig hikariConfig) {
		hikariConfig.setJdbcUrl(readJdbcProperties.getUrl());
		hikariConfig.setUsername(readJdbcProperties.getUsername());
		hikariConfig.setPassword(readJdbcProperties.getPassword());
		hikariConfig.setDriverClassName(readJdbcProperties.getDriverClassName());

		return new HikariDataSource(hikariConfig);
	}

	@Bean("routingDataSource")
	public DataSource routingDataSource(
		@Qualifier("mainDatasource") DataSource mainDataSource,
		@Qualifier("readDatasource") DataSource readDataSource) {

		RoutingDataSource routingDataSource = new RoutingDataSource(); // AbstractRoutingDataSource

		Map<Object, Object> dataSourceMap = new HashMap<>();
		dataSourceMap.put("master", mainDataSource);
		dataSourceMap.put("slave", readDataSource);

		routingDataSource.setTargetDataSources(dataSourceMap);
		routingDataSource.setDefaultTargetDataSource(mainDataSource);

		return routingDataSource;
	}

	@Primary
	@Bean("dataSource")
	public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
		return new LazyConnectionDataSourceProxy(routingDataSource);
	}

	@Bean("entityManagerFactory")
	public EntityManagerFactory entityManagerFactory(@Qualifier("dataSource") DataSource dataSource) {

		LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
		factory.setPackagesToScan("com.moonsiri.**.domain");
		factory.setDataSource(dataSource);
		factory.setPersistenceUnitName("MySQL");

		HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
		vendorAdapter.setShowSql(true);
		factory.setJpaVendorAdapter(vendorAdapter);

		Map<String, Object> properties = new HashMap<>();
		properties.put("hibernate.hbm2ddl.auto", "none");
		properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
		properties.put("hibernate.format_sql", true);
		properties.put("hibernate.show_sql", false);  // sql은 log4j로 출력 org.hibernate.SQL=DEBUG
		properties.put("hibernate.globally_quoted_identifiers", true);  // 예약어 컬럼명 사용 허용
		factory.setJpaPropertyMap(properties);
		factory.afterPropertiesSet();

		return factory.getObject();
	}

	@Bean("jpaTxManager")
	public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
		JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
		jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);

		return jpaTransactionManager;
	}
}

LazyConnectionDataSourceProxy는 트랜잭션 시작 시 Connection Proxy 객체를 리턴하고 실제 쿼리를 실행할 때 가서야 데이터 소스에서 connection을 가져옵니다.

 

이렇게 모든 설정을 마치면 다음과 같이 readOnly 속성으로 분기처리 할  수 있습니다.

@Transactional(readOnly = true)

 

 

 

[Reference]

https://moonsiri.tistory.com/92

https://mudchobo.github.io/posts/spring-boot-jpa-master-slave

https://mudchobo.github.io/posts/spring-boot-jpa-multiple-database

728x90
반응형

댓글