본문 바로가기
우리 FISA

우리FISA 클라우드 서비스 개발 - Spring Boot 어플리케이션에서 RDS MySQL 이중화

by dvid 2023. 8. 29.

DB Replication

읽기 부하분산과 데이터 백업을 위해 데이터베이스 이중화를 진행했다.

개발환경은 아래와 같다.

  • Spring boot 2.7.14
  • MySQL 8.0.33 (RDS)
  • Spring Data JPA

 

RDS 설정

현재는 이미 복제가 완료 되었지만, 손쉽게 진행할 수 있다.

버튼 하나만 누르면 Master DB와 DB 계정과 데이터부터 보안그룹까지 똑같은 환경으로 복제된다.

Master DB는 쓰기와 수정 전용, 복제본은 읽기 전용으로 활용한다.

 

Spring application.yml 수정

spring:
  datasource:
    master:
      hikari:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://{master db url}:{port}/{schema}
        readOnly: false
        username: {username}
        password: {password}
    replica:
      hikari:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://{replica db url}:{port}/{schema}
        readOnly: true
        username: {username}
        password: {password}

 

메인 DB, 레플리카 DB 설정

@Configuration
public class DataSourceConfig {

	@Primary
	@Bean(MASTER_DATE_SOURCE)
	@ConfigurationProperties(prefix = MASTER_PREFIX)
	public DataSource masterDataSource() {
		return DataSourceBuilder
			.create()
			.type(HikariDataSource.class)
			.build();
	}

	@Bean(REPLICA_DATE_SOURCE)
	@ConfigurationProperties(prefix = REPLICA_PREFIX)
	public DataSource replicaDataSource() {
		return DataSourceBuilder
			.create()
			.type(HikariDataSource.class)
			.build();
	}

}

yml을 적절히 읽어 설정해준다.

 

읽기 / 쓰기 설정

public enum DataSourceType {

	MASTER,
	REPLICA;

}
public class RoutingDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
			return DataSourceType.REPLICA;
		}
		return DataSourceType.MASTER;
	}

}

 

@EnableJpaRepositories(
	basePackages = BASE_PACKAGES,
	entityManagerFactoryRef = ENTITY_MANAGER_FACTORY,
	transactionManagerRef = TRANSACTION_MANAGER
)
@Configuration
public class RoutingDataSourceConfig {

	@Bean(ROUTING_DATA_SOURCE)
	public DataSource routingDataSource(
		@Qualifier(MASTER_DATE_SOURCE) final DataSource master,
		@Qualifier(REPLICA_DATE_SOURCE) final DataSource replica
	) {

		RoutingDataSource routingDataSource = new RoutingDataSource();

		Map<Object, Object> dataSourceMap = new HashMap<>();
		dataSourceMap.put(DataSourceType.MASTER, master);
		dataSourceMap.put(DataSourceType.REPLICA, replica);

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

		return routingDataSource;
	}

	@Bean(DATA_SOURCE)
	public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE) DataSource routingDataSource) {
		return new LazyConnectionDataSourceProxy(routingDataSource);
	}

	@Bean(ENTITY_MANAGER_FACTORY)
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
		@Qualifier(DATA_SOURCE) DataSource dataSource) {

		LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();

		entityManagerFactory.setDataSource(dataSource);
		entityManagerFactory.setPackagesToScan(BASE_PACKAGES);
		entityManagerFactory.setJpaVendorAdapter(this.jpaVendorAdapter());
		entityManagerFactory.setPersistenceUnitName(ENTITY_MANAGER);

		return entityManagerFactory;
	}

	private JpaVendorAdapter jpaVendorAdapter() {
		HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
		adapter.setGenerateDdl(false);
		adapter.setShowSql(false);

		adapter.setDatabasePlatform(HIBERNATE_DIALECT);

		return adapter;
	}

	@Bean(TRANSACTION_MANAGER)
	public PlatformTransactionManager platformTransactionManager(
		@Qualifier(ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean emf
	) {

		JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
		jpaTransactionManager.setEntityManagerFactory(emf.getObject());

		return jpaTransactionManager;
	}

}

 

LazyConnctionDataSourceProxy

Spring에서는 @Transaction에 진입하면 자동으로 DB 커넥션을 사용한다.

따라서 DataSource가 여러개라면 실제 커넥션이 필요할 때 DataSource를 선택하지 못한다.

LazyConnectionDataSourceProxy을 사용하면 실제로 필요한 시점에만 커넥션을 점유하게 할 수 있다.

읽기 / 쓰기 전용 DB가 나눠져 있을 때 유용하게 쓰일 수 있다.

 

상수

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DbConstUtil {

	public static final String PROFILE_PROD = "prod";
	public static final String BASE_PACKAGES = "xyz.iwasacar.api.domain";

	public static final String MASTER_DATE_SOURCE = "masterDataSource";
	public static final String REPLICA_DATE_SOURCE = "replicaDataSource";
	public static final String MASTER_PREFIX = "spring.datasource.master.hikari";
	public static final String REPLICA_PREFIX = "spring.datasource.replica.hikari";

	public static final String ROUTING_DATA_SOURCE = "routingDataSource";
	public static final String DATA_SOURCE = "dataSource";

	public static final String ENTITY_MANAGER_FACTORY = "entityManagerFactory";
	public static final String TRANSACTION_MANAGER = "transactionManager";
	public static final String ENTITY_MANAGER = "entityManager";
	public static final String HIBERNATE_DIALECT = "org.hibernate.dialect.MySQL8Dialect";

}

 

처음 yml을 설정할 때 replica의 depth가 잘못되어 조금 헤맸다. 이 점을 주의하자.

또한, RDS를 활용해서 쉽게 복제된 DB를 만들 수 있었는데, 나중에는 이 작업도 진행해보고 싶다.

댓글