Spring Data JPA multiple DataSource 사용

사실 DataSource는 대부분 하나만 씁니다.
하지만, Database가 사용량이 넘처나면 분리할 수 있는 domain별로 하나씩 분리하게 됩니다.. 그러다가 더 사이즈가 감당이 안되면 sharding을 해서 쓰기도 하지요. 어쨌든, 여러가의 DataSource로 분기가 된다는것은, 종류별로 Datasource를 inject 해야하고, 또한 종류별로 @Transactional을 걸어주어야 합니다. 하긴 aop로 적절히 묶어줘도 되겠네요.

여기까지는 봐주겠지만, JPA를 쓰게 된다면 문제가 더 복잡해집니다. Datasource마다 또다른 EntityManagerFactory를 가져야하고, 그마다 @Entity도 구분이 되야 합니다. 하나라도 틀렸을경우는 Exception이 용서치 않습니다. 힘듭니다. 방법은 하나 Datasource도 하나, EntityManagerFactory도 하나 TransactionManager도 하나로 쓰는것 입니다. 안된다구요? 아뇨 됩니다.

그전에 우선 hibernate가 구동 될때, hbm2ddl.auto를 사용하지 않아야 합니다. DataSource는 runtime에 동적 결정임으로 hibernate구동시 MetaData사용은 의미가 없습니다. 에러만 유발하겠죠.

entityManagerFactory 설정

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setShowSql(true);
        //hibernateJpaVendorAdapter.setGenerateDdl(true);

        HashMap<String, Object> jpaMap = new HashMap<String, Object>();
        jpaMap.put("hibernate.format_sql", true);
        //jpaMap.put("hibernate.hbm2ddl.auto", "validate");
        jpaMap.put("hibernate.cache.use_second_level_cache", false);
        jpaMap.put("hibernate.cache.use_query_cache", false);
        jpaMap.put("hibernate.use_sql_comments", false);
        jpaMap.put("hibernate.connection.autocommit", false);

        LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        localContainerEntityManagerFactoryBean.setMappingResources("META-INF/orm.xml");
        localContainerEntityManagerFactoryBean.setDataSource(routingDataSource());
        localContainerEntityManagerFactoryBean.setPackagesToScan("com.skp.comments.was.domain.model");
        localContainerEntityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter);
        localContainerEntityManagerFactoryBean.setJpaPropertyMap(jpaMap);
        return localContainerEntityManagerFactoryBean;
    }

Spring에서는 기본적으로 AbstractRoutingDataSource라는 class를 제공합니다. 대표 DataSource를 설정하고, 조건에 따라 선택될 targetDataSource collection을 설정하면 사용할 수 있습니다.

routingDataSource 설정

    @Bean
    public DataSource routingDataSource() {
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return databaseRoutingVariableScope().getCurrentValue();
            }
        };
        routingDataSource.setDefaultTargetDataSource(dataSource());
        Map<Object, Object> param = new HashMap<Object, Object>();
        param.put(DatabaseRouting.DATABASE_PLAIN, dataSource());
        param.put(DatabaseRouting.DATABASE_COMMENTS_SHARD, commentsDataSource());
        param.put(DatabaseRouting.DATABASE_USER_SHARD, userDataSource());
        param.put(DatabaseRouting.DATABASE_APP_SHARD, appDataSource());
        routingDataSource.setTargetDataSources(param);
        return routingDataSource;
    }

사실 Spring Data Jpa에서 만들어진 repository들은 그대로 직접 쓰지 못합니다. 어떤 DataSource를 써야하는지 결정을 먼저 하고 나서 써야 가능한 구조입니다. 그래서 code를 around해줘야하는 class를 이용해야합니다.

public class MultipleJpaRepository implements MultipleJpaRepositoryIF, InitializingBean {

    private DatabaseRouting databaseRouting;
    private LocalVariableScope<DatabaseRouting> databaseRoutingLocalVariableScope;

    public void setDatabaseRouting(DatabaseRouting databaseRouting) {
        this.databaseRouting = databaseRouting;
    }

    public void setDatabaseRoutingLocalVariableScope(LocalVariableScope<DatabaseRouting> databaseRoutingLocalVariableScope) {
        this.databaseRoutingLocalVariableScope = databaseRoutingLocalVariableScope;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(databaseRouting);
        Assert.notNull(databaseRoutingLocalVariableScope);
    }

    @Override
    public <T> T execute(String key, DatabaseType databaseType, AbstractMultipleJpaRepository.Executor<T> ex) {
        Assert.notNull(key, "key must be not null");

        try {
            databaseRoutingLocalVariableScope.setValue(databaseRouting);

            return ex.execute();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            databaseRoutingLocalVariableScope.removeValue();
        }
    }

}

jpa가 생성한 repository들은 Executor interface를 anonymous class로 생성을 하여 코드를 주입해야 합니다.

public class BlockUserRepositoryImpl extends AbstractMultipleJpaRepository implements BlockUserRepository {

    private BlockUserJpaRepository blockUserJpaRepository;

    public void setBlockUserJpaRepository(BlockUserJpaRepository blockUserJpaRepository) {
        this.blockUserJpaRepository = blockUserJpaRepository;
    }

    ...

    @Override
    public BlockUser findOne(final BlockUserId blockUserId) {
        return execute(blockUserId.getAppId(), DatabaseType.READ_ONLY, new Executor<BlockUser>() {
            @Override
            public BlockUser execute() {
                return blockUserJpaRepository.findOne(blockUserId);
            }
        });
    }
    ...
}

위처럼 execute를 잘 쓰면 됩니다. 사실 전 AbstractMultipleJpaRepository 추상 클래스를 껍대기로 만들고 MultipleJpaRepository를 주입해서 위임으로 사용하고 있습니다. 이유는 여러 종류의 multipleJpaRepository를 Bean으로 생성하고 개개의 repository마다 주입해서 쓰려고 했고, execute와 Executor인터페이스를 편하게 쓰려고 했을뿐입니다.

@Configuration
@EnableJpaRepositories(basePackages = {"com.skp.comments.was.jpaRepository"}, entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "routingTransactionManager")
public class RepositoryConfig {
    
    ...

    @Bean
    public PlatformTransactionManager routingTransactionManager() {
        return new JpaTransactionManager(entityManagerFactory().getObject());
    }

    ...

    @Bean
    public BlockUserRepository blockUserRepository() {
        BlockUserRepositoryImpl impl = new BlockUserRepositoryImpl();
        impl.setBlockUserJpaRepository(ac.getBean(BlockUserJpaRepository.class));
        impl.setMultipleJpaRepositoryIF(databaseConfig.appMultipleJpaRepositoryIF());
        return impl;
    }

    ...

}

마지막으로 @EnableJpaRepositories로 jpa가 생성할 repository를 등록해줄때 entityManagerFactory와 transactionManager를 지정해주면 만사 해결!