티스토리 뷰

프록시 객체와 지연 로딩으로 DataSource 분기 처리 실패 해결하기

Overview

저번 글에서는 부하 분산을 위해 MySQL Replication 구성을 사용했고, 어플리케이션에서는 쿼리 요청에 따라 마스터 서버와 슬레이브 서버로 분기하도록 코드를 작성했습니다. 그러나 작성한 코드를 실행해보면 AbstractRoutindDataSource 에서 올바른 DataSourceConnection 객체를 얻어오지 못합니다. 이번 글에서는 DataSource 의 분기 처리가 실패하는 이유에 대해서 알아보고 이를 해결할 수 있는 방법을 살펴봅니다.

 

왜 분기 처리가 실패하는 걸까?

분기 처리가 실패하는 이유를 알아보려면 먼저 스프링의 트랜잭션 동기화에 대해서 알아야 합니다. 트랜잭션 동기화란 트랜잭션을 시작하기 위해 만든 Connection 객체를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO 메소드에서 저장된 Connection 객체를 가져다가 사용하는 것입니다. 즉, 몇 번의 쿼리가 반복되더라도 Connection 을 종료시키지 않고, 보관해둔 뒤 다음 과정에서도 계속 이용하면서 하나의 트랜잭션으로 묶는 것입니다.

 

 

트랜잭션 동기화는 위의 그림에 나와있는 순서대로 진행됩니다. 여기서 문제가 되는 것은 Connection 객체를 획득하는 시기입니다. 우리는 AbstractRoutingDataSource 에서 트랜잭션 속성에 따라 DataSource 를 분기했으며, 이에 따라 Connection 객체를 리턴할 수 있도록 코드를 작성했습니다. 이 때, 트랜잭션 속성은 TransactionSynchronizationManager 가 관리합니다. 그러나 TransactionSynchronizationManager 는 말 그대로 트랜잭션 동기화에 대한 관리자이기 때문에 동기화가 시작되기 전에는 해당 트랜잭션 속성을 알 수 없습니다. 그렇기 때문에 분기 처리가 실패하는 것입니다.

 

그럼 어떻게 해결할 수 있을까?

문제는 스프링이 트랜잭션이 걸리기만 하면 Connection 객체를 얻으려고 하기 때문에 발생합니다. 따라서, 아래 그림과 같이 TransactionManager 에게 AbstractRoutingDataSource 에 대한 프록시 객체를 리턴하게 만들어 주면 됩니다.

 

 

LazyConnectionDataSourceProxy 를 사용하면 실제 쿼리를 호출하기 전까지 JDBC Connection 을 가져오지 않습니다. 따라서, 트랜잭션 동기화가 시작되고 대상 메소드 안에서 쿼리가 발생한 후에 우리가 AbstractRoutingDataSource 에 작성한 로직대로 올바른 DataSource 에 대한 Connection 을 얻어 쿼리를 실행할 수 있습니다.

 

Refactoring: 프록시 적용하기

먼저 AbstractRoutingDataSource 에 대한 프록시 객체를 생성하고 기존에 AbstractRoutingDataSource 를 사용하고 있던 메소드에서 프록시 객체를 사용할 수 있도록 수정합니다.

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:/db-secret.properties")
public class DataSourceConfig {
    ...
    // AbstractRoutingDataSource에 대한 프록시 생성
    @Bean(name = "proxyDataSource")
    public DataSource proxyDataSource(@Qualifier(value = "routingDataSource") DataSource routingDataSource) {

        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    // TranscationManager가 프록시 객체를 사용할 수 있도록 dataSource 설정
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier(value = "proxyDataSource") DataSource dataSource) {

        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);

        return transactionManager;
    }
}

 

@Configuration
@MapperScan(basePackages = "me.liiot.snsserver.mapper")
public class MyBatisConfig {
    ...
    // SqlSessionFactory가 프록시 객체를 사용할 수 있도록 dataSource 설정
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "proxyDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
            applicationContext.getResources("classpath:/mapper/**/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
    ...
}

 

After Refactoring

프록시 객체와 지연 로딩을 통해 실패 없이 쿼리에 따라 DataSource 를 분기할 수 있게 되었습니다.

 

 

 


진행 프로젝트

github.com/f-lab-edu/sns-itda

 

f-lab-edu/sns-itda

Contribute to f-lab-edu/sns-itda development by creating an account on GitHub.

github.com

 

이어지는 글

chagokx2.tistory.com/100

 

부하 분산을 위한 MySQL Replication 구성 및 쿼리 요청 분기

부하 분산을 위한 MySQL Replication 구성 및 쿼리 요청 분기 Overview 내가 진행하고 있는 SNS 프로젝트를 보면 Service Layer에 속해있는 거의 모든 메소드가 데이터베이스에 쿼리 요청을 보낸다. 나의 프

chagokx2.tistory.com

 

댓글