-
[Spring boot] Database가 Replication일 때 DataSource 설정Spring Boot/기타 2023. 1. 17. 23:25반응형
1. Replication 란?
Master/Slave 관계를 설정하고 데이터 원본은 Master, 데이터 사본은 Slave에 저장한 후 Master에서는 Write(Insert, Update, Delete) 작업을 Slave에서는 Read(Select) 작업을 처리하여 부하를 분산시키는 기술입니다.
2. DataSource 설정@Transactional(readOnly = true | false) 을 통해 Database Replication일 때 DataSource를 설정할 수 있습니다.
@Transactional(readOnly = false) 일 때는 Master DB에서 처리
@Transactional(readOnly = true) 일 때는 Slavee DB에서 처리
application.ymlspring: datasource: master: hikari: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sample read-only: false username: sample_user password: password1! slave: hikari: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/sample read-only: true username: sample_user password: password1!
DataSourceConfig.javapackage com.example.datasourcereplication.config; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @Slf4j @Configuration public class DataSourceConfig { private final String MASTER_DATA_SOURCE = "masterDataSource"; private final String SLAVE_DATA_SOURCE = "slaveDataSource"; @Primary @Bean(MASTER_DATA_SOURCE) @ConfigurationProperties(prefix = "spring.datasource.master.hikari") public DataSource masterDataSource() { return DataSourceBuilder .create() .type(HikariDataSource.class) .build(); } @Bean(SLAVE_DATA_SOURCE) @ConfigurationProperties(prefix = "spring.datasource.slave.hikari") public DataSource slaveDataSource() { return DataSourceBuilder .create() .type(HikariDataSource.class) .build(); } }
DataSourceType.java
동적으로 DataSource를 변경하려면 AbstractRoutingDataSource.setTargetDataSources(Map<Object, Object> targetDataSources)를 설정이 필요한데, 이때 DataSource를 Map 형태로 전달하기 위해 필요한 key 값을 정의한 enum입니다.package com.example.datasourcereplication.common.type; public enum DataSourceType { MASTER, SLAVE }
RoutingDataSource.java
AbstractRoutingDataSource.determineCurrentLookupKey()를 구현할 때 @Transactional(readOnly = true) 여부에 따라서 DataSource를 결정할 수 있도록 구현하세요.package com.example.datasourcereplication.config; import com.example.datasourcereplication.common.type.DataSourceType; 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() ? DataSourceType.SLAVE : DataSourceType.MASTER; } }
RoutingDataSourceConfig.java
스프링은 트랜잭션에 진입하는 순간 설정된 DataSource에서 Connection을 가져오기 때문에, Multi DataSource 환경에서 트랜잭션에 진입한 이후 DataSource 분기가 불가능합니다.
LazyConnectionDataSourceProxy을 사용하면 실제로 필요한 시점에 DataSource에서 Connection을 가져올 수 있습니다.package com.example.datasourcereplication.config; import com.example.datasourcereplication.common.type.DataSourceType; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; @Slf4j @EnableJpaRepositories( basePackages = "com.example.datasourcereplication.domain.repository", entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager" ) @Configuration public class RoutingDataSourceConfig { private final String ROUTING_DATA_SOURCE = "routingDataSource"; private final String MASTER_DATA_SOURCE = "masterDataSource"; private final String SLAVE_DATA_SOURCE = "slaveDataSource"; private final String DATA_SOURCE = "dataSource"; @Bean(ROUTING_DATA_SOURCE) public DataSource routingDataSource( @Qualifier(MASTER_DATA_SOURCE) final DataSource masterDataSource, @Qualifier(SLAVE_DATA_SOURCE) final DataSource slaveDataSource) { RoutingDataSource routingDataSource = new RoutingDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put(DataSourceType.MASTER, masterDataSource); dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource); routingDataSource.setTargetDataSources(dataSourceMap); routingDataSource.setDefaultTargetDataSource(masterDataSource); return routingDataSource; } @Bean(DATA_SOURCE) public DataSource dataSource( @Qualifier(ROUTING_DATA_SOURCE) DataSource routingDataSource) { return new LazyConnectionDataSourceProxy(routingDataSource); } @Bean("entityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier(DATA_SOURCE) DataSource dataSource) { LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); entityManagerFactory.setDataSource(dataSource); entityManagerFactory.setPackagesToScan("com.example.datasourcereplication.domain.entity"); entityManagerFactory.setJpaVendorAdapter(this.jpaVendorAdapter()); entityManagerFactory.setPersistenceUnitName("entityManager"); return entityManagerFactory; } private JpaVendorAdapter jpaVendorAdapter() { HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter(); hibernateJpaVendorAdapter.setGenerateDdl(false); hibernateJpaVendorAdapter.setShowSql(false); hibernateJpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5InnoDBDialect"); return hibernateJpaVendorAdapter; } @Bean("transactionManager") public PlatformTransactionManager platformTransactionManager( @Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) { JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); jpaTransactionManager.setEntityManagerFactory(entityManagerFactory.getObject()); return jpaTransactionManager; } }
3. 단위 테스트3_1. DataSource 설정 테스트
package com.example.datasourcereplication.config; import static org.junit.jupiter.api.Assertions.assertEquals; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.env.Environment; @Slf4j @SpringBootTest class DataSourceConfigTest { @Autowired private Environment environment; @DisplayName("MasterDataSource 설정 테스트") @Test void masterDataSourceTest( @Qualifier("masterDataSource") final DataSource masterDataSource) { // Given String driverClassName = environment.getProperty("spring.datasource.master.hikari.driver-class-name"); String jdbcUrl = environment.getProperty("spring.datasource.master.hikari.jdbc-url"); Boolean readOnly = Boolean.valueOf(environment.getProperty("spring.datasource.master.hikari.read-only")); String username = environment.getProperty("spring.datasource.master.hikari.username"); // When try (HikariDataSource hikariDataSource = (HikariDataSource) masterDataSource) { // Then log.info("hikariDataSource : [{}]", hikariDataSource); assertEquals(hikariDataSource.getDriverClassName(), driverClassName); assertEquals(hikariDataSource.getJdbcUrl(), jdbcUrl); assertEquals(hikariDataSource.isReadOnly(), readOnly); assertEquals(hikariDataSource.getUsername(), username); } } @DisplayName("SlaveDataSource 설정 테스트") @Test void slaveDataSourceTest( @Qualifier("slaveDataSource") final DataSource slaveDataSource) { // Given String driverClassName = environment.getProperty("spring.datasource.slave.hikari.driver-class-name"); String jdbcUrl = environment.getProperty("spring.datasource.slave.hikari.jdbc-url"); Boolean readOnly = Boolean.valueOf(environment.getProperty("spring.datasource.slave.hikari.read-only")); String username = environment.getProperty("spring.datasource.slave.hikari.username"); // When try (HikariDataSource hikariDataSource = (HikariDataSource) slaveDataSource) { // Then log.info("hikariDataSource : [{}]", hikariDataSource); assertEquals(hikariDataSource.getDriverClassName(), driverClassName); assertEquals(hikariDataSource.getJdbcUrl(), jdbcUrl); assertEquals(hikariDataSource.isReadOnly(), readOnly); assertEquals(hikariDataSource.getUsername(), username); } } }
3_2. Replication 설정 테스트package com.example.datasourcereplication.config; import static org.junit.jupiter.api.Assertions.assertEquals; import com.example.datasourcereplication.common.type.DataSourceType; import java.lang.reflect.Method; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; @Slf4j @SpringBootTest class RoutingDataSourceConfigTest { private static final String DETERMINE_CURRENT_LOOKUP_KEY = "determineCurrentLookupKey"; @Transactional(readOnly = false) @DisplayName("MasterDataSource Replication 설정 테스트") @Test void testMasterDataSourceReplication() throws Exception { // Given RoutingDataSource routingDataSource = new RoutingDataSource(); // When Method declaredMethod = RoutingDataSource.class.getDeclaredMethod(DETERMINE_CURRENT_LOOKUP_KEY); declaredMethod.setAccessible(true); Object object = declaredMethod.invoke(routingDataSource); // Then log.info("object : [{}]", object); assertEquals(DataSourceType.MASTER.toString(), object.toString()); } @Transactional(readOnly = true) @DisplayName("SlaveDataSource Replication 설정 테스트") @Test void testSlaveDataSourceReplication() throws Exception { // Given RoutingDataSource routingDataSource = new RoutingDataSource(); // When Method declaredMethod = RoutingDataSource.class.getDeclaredMethod(DETERMINE_CURRENT_LOOKUP_KEY); declaredMethod.setAccessible(true); Object object = declaredMethod.invoke(routingDataSource); // Then log.info("object : [{}]", object); assertEquals(DataSourceType.SLAVE.toString(), object.toString()); } }
3_3. Transaction 테스트
TodoServiceTest.testInsertTodoReadOnlyTrue() 실행 시 Exception이 발생하여 정상적으로 롤백되는지 확인하세요.TodoService.java
package com.example.datasourcereplication.service; import com.example.datasourcereplication.domain.entity.Todo; import com.example.datasourcereplication.domain.repository.TodoRepository; import com.example.datasourcereplication.dto.TodoRequestDto; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Slf4j @RequiredArgsConstructor @Service public class TodoService { private final TodoRepository todoRepository; /** * To-Do 한건 저장 */ @Transactional(readOnly = false) public int insertTodoReadOnlyFalse(TodoRequestDto todoRequest) { int result = 0; todoRepository.save(Todo.builder() .title(todoRequest.getTitle()) .description(todoRequest.getDescription()) .completed(todoRequest.getCompleted()) .build()); result++; return result; } /** * To-Do 한건 저장 */ @Transactional(readOnly = true) public int insertTodoReadOnlyTrue(TodoRequestDto todoRequest) { int result = 0; todoRepository.save(Todo.builder() .title(todoRequest.getTitle()) .description(todoRequest.getDescription()) .completed(todoRequest.getCompleted()) .build()); result++; return result; } }
TodoServiceTest.java
package com.example.datasourcereplication.service; import static org.junit.jupiter.api.Assertions.assertThrows; import com.example.datasourcereplication.dto.TodoRequestDto; import java.util.ArrayList; import java.util.List; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest class TodoServiceTest { @Resource TodoService todoService; @DisplayName("insertTodo_@Transactional(readOnly = true)일 때 한건 저장") @Test void testInsertTodoReadOnlyFalse() { // Given TodoRequestDto todoRequest = TodoRequestDto.builder() .title("Title Junit Test Insert 01") .description("Description Junit Test Insert 02") .completed(false) .build(); // When int result = todoService.insertTodoReadOnlyFalse(todoRequest); log.info("result:{}", result); // Then Assertions.assertEquals(1, result); } @DisplayName("insertTodoReadOnlyTrue_@Transactional(readOnly = true)일 때 한건 저장") @Test void testInsertTodoReadOnlyTrue() { // Given TodoRequestDto todoRequest = TodoRequestDto.builder() .title("Title Junit Test") .description("Description Junit Test") .completed(false) .build(); // When & Then Exception exception = assertThrows(Exception.class, () -> todoService.insertTodoReadOnlyTrue(todoRequest)); log.info("exception:{}", exception); } }
소스 코드는 Github Repository - https://github.com/tychejin1218/blog/tree/main/datasource-replication 를 참조하세요.반응형'Spring Boot > 기타' 카테고리의 다른 글
[Spring Boot] RabbitMQ 연동하기 (0) 2023.06.04 [Spring Boot] Amazon S3로 파일 업로드 및 삭제 (0) 2023.01.27 [Spring Boot] Spring Data JPA + QueryDSL 설정 (2) 2023.01.15 [Spring Boot] 유효성 검사 처리 (Custom Validation) (0) 2022.06.06 [Spring Boot] 에러 메시지 처리 (Custom Exception) (0) 2022.06.06