본문 바로가기
BE/Spring

Spring에서 AWS RDS MySQL Replication 적용하기

by cjsrhd94 2022. 11. 29.

현재 진행하고 있는 개인 프로젝트에서 DB로 AWS RDS MySQL 한 대만 사용하고 있다. 캐시 서버를 운용하여 DB에 가해지는 부하를 줄인다고 하더라도 트래픽의 증가에 따라 한 대 뿐인 데이터베이스에 장애가 발생할 가능성이 있다. 이에 이러한 상황에 대비하기 위해 AWS RDS 환경에서 MySQL Replication을 적용해보았다.

 

이해를 돕기위해 실제 프로젝트에 사용한 깃허브 레포지토리의 패키지 주소를 공유합니다. 아래 주소를 참고해주세요.

https://github.com/ProjectShallWe/shallwe-backend/tree/main/src/main/java/com/project/board/global/datasource

 

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/

댓글