현재 진행하고 있는 개인 프로젝트에서 DB로 AWS RDS MySQL 한 대만 사용하고 있다. 캐시 서버를 운용하여 DB에 가해지는 부하를 줄인다고 하더라도 트래픽의 증가에 따라 한 대 뿐인 데이터베이스에 장애가 발생할 가능성이 있다. 이에 이러한 상황에 대비하기 위해 AWS RDS 환경에서 MySQL Replication을 적용해보았다.
이해를 돕기위해 실제 프로젝트에 사용한 깃허브 레포지토리의 패키지 주소를 공유합니다. 아래 주소를 참고해주세요.
AWS RDS 설정
기존 RDS의 읽기 전용 DB 하나를 생성해준다.
기존의 RDS와 동일한 설정이 되어있기 때문에 추가로 설정을 변경할 부분은 없다. 바로 생성해준다.
Spring에서 Replication 관련 코드 작성
ReplicationRoutingConstants
@Getter
@RequiredArgsConstructor
public enum ReplicationRoutingConstants {
MASTER("master"),
SLAVE("slave");
public final String type;
}
RoutingDataSource
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (isReadOnly) {
logger.info("Connect SLAVE");
return SLAVE.type;
} else {
logger.info("Connect MASTER");
return MASTER.type;
}
}
}
RoutingDataSource 클래스에서는 현재 요청된 트랜잭션이 읽기 전용인지 판단하여 하단의 DataSource내 dataSourcesMap의 조회 키로 사용할 값을 return한다.
application-rds.yml
spring:
datasource:
master:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://${AWS_endpoint}:${port}/${db_name}
username: ${username}
password: ${password}
slave:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://${AWS_endpoint}:${port}/${db_name}
username: ${username}
password: ${password}
yaml 파일내 DB설정을 위와 같이 해준다. AWS RDS 설정에 따라 위 변수들을 작성한다.
DataSource
@Configuration
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) // AutoConfiguration이 되지 않도록 설정
@EnableTransactionManagement // 어노테이션 기반의 트랜잭션 기능 활성화
@EnableJpaRepositories(basePackages = {"com.project.board"})
public class DataSourceConfig {
@Bean
// yaml 파일에서 해당 속성 값을 가져온다.
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
// 특정 빈을 선택하여 주입하기 위해 @Qualifier 사용
public DataSource routingDataSource(@Qualifier(value = "masterDataSource") DataSource masterDataSource,
@Qualifier(value = "slaveDataSource") DataSource slaveDataSource) {
AbstractRoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
// dataSourceMap 객체에 분기할 서버들의 DataSource빈을 저장
dataSourceMap.put(MASTER.type, masterDataSource);
dataSourceMap.put(SLAVE.type, slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
// AbstractRoutingDataSource에 대한 프록시 생성, @Primary 사용으로 우선 적용
@Primary
@Bean
public DataSource proxyDataSource(@Qualifier(value = "routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
// TransactionManager가 프록시 객체를 사용하도록 dataSource 설정
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(proxyDataSource(routingDataSource(masterDataSource(), slaveDataSource())));
}
}
@EnableAutoConfiguration의 exclude 옵션으로 어플리케이션 실행시 자동으로 설정되던 DataSourceAutoConfiguration을 제외하고, 대신 DataSourceConfig를 불러올 수 있게 한다.
DataSourceConfig 클래스에서 각각의 DB에 대한 DataSource 빈을 등록한다. 해당 빈의 설정 정보는 application-rds.yml 파일에서 가져온다.
RoutingDataSource 빈은 쿼리 요청에 따라 적절한 서버로 분기하는 데 사용한다. DataSource에 어떤 빈을 주입할지 선택하기 위해 @Qualifier 어노테이션을 사용한다.
위 코드에서 가장 주목할 부분은 맨 아래 있는 transactionManager 빈 부분이다. 해당 빈은 routingDataSource가 아닌 proxyDataSource를 argument로 받아 DataSourceTransactionManager를 생성하고 있다. 위와 같이 프록시를 설정해주지 않으면 분기 처리가 실패한다. 이유를 알기 위해서는 스프링의 트랜잭션 동기화에 대해 알아야한다.
스프링은 트랜잭션을 시작할 때 쿼리가 실행되기 전 DataSource를 결정한다.그리고 해당 DataSource로 트랜잭션 메서드 내 모든 쿼리를 수행한다. 따라서 우리가 설정한대로 분기 처리를 하려면 실제로 쿼리를 실행할 때 DataSource를 정할 수 있어야 한다. 이를 위해 LazyConnectionDataSourceProxy로 Datasource 객체를 감싸서 DataSourceTransactionManager에서 프록시 객체를 사용할 수 있게 해줘야 한다.
위와 같이 설정시 @Transactional(readOnly = true)일 때 정상적으로 slave 서버에서 쿼리를 실행하는 것을 확인할 수 있다.
참고
김영한 스프링 DB1편 - 데이터 접근 핵심 원리
https://baekjungho.github.io/wiki/spring/spring-lazyconnectiondatasourceproxy/
'BE > Spring' 카테고리의 다른 글
ApplicationEventPublisher를 활용하여 서비스 강결합 문제 해결하기 (0) | 2023.01.31 |
---|---|
스프링 S3 연동 오류: No valid instance id defined (0) | 2022.07.27 |
본인 확인은 어떤 layer에서 이루어져야 할까? (0) | 2022.07.22 |
yaml 파일을 그룹으로 관리하기 (0) | 2022.07.09 |
Springboot에서 Redis Cache 적용하기 (0) | 2022.06.27 |
댓글