이번 글에서는 기존 단일 데이터소스 구성에서 데이터베이스 접근을 쓰기(Write)와 읽기(Read)로 분리하고, 각각을 MySQL Router의 6446, 6447 포트로 연결해서 로드밸런싱하는 구조를 구현한 과정을 공유합니다.
기존에는 하나의 URL로만 DB에 연결했습니다.
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.dbcp2.username}")
private String username;
@Value("${spring.datasource.dbcp2.password}")
private String password;
@Bean
public BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 커넥션 풀 최적화
dataSource.setInitialSize(18);
dataSource.setMaxTotal(18);
dataSource.setMaxIdle(18);
dataSource.setMinIdle(18);
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery("SELECT 1");
return dataSource;
}
}
application.yml 설정spring:
datasource:
router:
url:
rw: jdbc:mysql://mysql-router:6446/eventor
ro: jdbc:mysql://mysql-router:6447/eventor
dbcp2:
username: ${MYSQL_USERNAME}
password: ${MYSQL_PASSWORD}
@Bean
@Profile("prod")
public DataSource writeDataSource(@Value("${spring.datasource.router.url.rw}") String url) {
return createDataSource(url);
}
@Bean
@Profile("prod")
public DataSource readDataSource(@Value("${spring.datasource.router.url.ro}") String url) {
return createDataSource(url);
}
spring.datasource.router.url.rw는 6446 포트 URL,spring.datasource.router.url.ro는 6447 포트 URL을 가리킵니다.@Bean
@Profile("prod")
public DataSource routingDataSource(
@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceContextHolder.WRITE, writeDataSource);
dataSourceMap.put(DataSourceContextHolder.READ, readDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(writeDataSource);
return routingDataSource;
}
@Primary
@Bean
@Profile("prod")
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
routingDataSource는 읽기/쓰기용 DataSource를 하나로 묶어 호출 시점에 적절한 DataSource를 선택하도록 합니다.LazyConnectionDataSourceProxy는 실제 커넥션 획득을 지연시켜 트랜잭션 시작 시점에 결정되게 합니다.public class DataSourceContextHolder {
public static final String READ = "READ";
public static final String WRITE = "WRITE";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setRoutingKey(String key) {
contextHolder.set(key);
}
public static String getRoutingKey() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
ThreadLocal에 저장해, 요청별로 독립적으로 관리합니다.@Aspect
@Component
public class DataSourceRoutingAspect {
@Before("@annotation(transactional)")
public void setDataSourceBasedOnTransaction(Transactional transactional) {
if (transactional.readOnly()) {
DataSourceContextHolder.setRoutingKey(DataSourceContextHolder.READ);
} else {
DataSourceContextHolder.setRoutingKey(DataSourceContextHolder.WRITE);
}
}
@After("@annotation(transactional)")
public void clearDataSourceContext(Transactional transactional) {
DataSourceContextHolder.clear();
}
}
@Transactional 애노테이션에 따라 읽기 전용 트랜잭션인지 확인해,READ 또는 WRITE 키를 DataSourceContextHolder에 세팅합니다.public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getRoutingKey();
}
}
AbstractRoutingDataSource를 상속해,이번 구성으로 WAS에서는 개발자는 특별히 데이터 소스를 신경 쓸 필요 없이,
@Transactional(readOnly = true) 만 붙여주면 MySQL Router를 통해 읽기/쓰기 서버가 적절히 분리되어 로드밸런싱 됩니다.
이런 아키텍처는 대규모 트래픽에서 DB 부하를 줄이고, 성능과 안정성을 크게 향상시키는 효과가 있습니다.