티스토리 뷰

최근에 DL(Data Lake) 데이터 저장소를 Logpresso에서 ClickHouse(+Nifi)로 전환하는 작업을 진행했다. 이에 따라 API 서버(Spring Boot)에서도 Logpresso에서 가지고 오던 데이터를 ClickHouse에서 가져오도록 전환을 진행했다. 이를 위해 하나의 프로젝트에 다중 데이터소스(pg, ClickHouse)를 연결해야 했다. 이 글은 그 내용을 다룬다.


1. build.gradle 에 clickhouse jdbc 추가

ClickHouse JDBC를 사용하기 위해 build.gradle에 의존성을 추가해준다.

 

2. yml 에 clickhouse 접속 정보 추가

application.yml 파일에 ClickHouse 접속 정보를 추가해준다.

  • datasource 위의 postgres depth 추가
  • clickhouse 접속 정보 추가

 

3. Configuration 파일 작성

두 개의 데이터베이스를 연결하기 위한 설정 파일을 작성해준다. 데이터베이스가 하나일 때는 Spring Boot에서 자동으로 설정해주지만, 두 개 이상일 때부터는 수동 설정이 필요하다.

3-1. Postgres Configuration 

먼저 기존에 사용하던 Postgres의 설정부터 해보자.

  • 우리는 디폴트로 Postgres를 사용하기 때문에 @Primary 어노테이션을 붙여줬다.
  • Postgres는 MyBatis와 JPA를 같이 사용하고 있기 때문에 두 설정을 모두 해줘야 한다.

1. 클래스 어노테이션 설정

  • @EnableJpaRepositories: JPA 리포지토리를 스캔하고 Spring 컨텍스트에 등록
    • basePackages: JPA 리포지토리 인터페이스가 위치한 패키지를 지정
    • entityManagerFactoryRef: 사용할 EntityManagerFactory의 빈 이름을 지정
    • transactionManagerRef: 사용할 TransactionManager의 빈 이름을 지정
  • @MapperScan: MyBatis 매퍼 인터페이스를 스캔하고 Spring 컨텍스트에 등록
    • sqlSessionFactoryRef: 사용할 SqlSessionFactory의 빈 이름을 지정
    • basePackages: MyBatis 매퍼 인터페이스가 위치한 패키지를 지정
@Configuration
@EnableJpaAuditing
@EnableJpaRepositories( // jpa
        // repository 위치
        basePackages = {"repository 위치", "여러개를", "적을수도", "있음"},
        entityManagerFactoryRef = "pgEntityManagerFactory",	// entityManagerFactory의 이름
        transactionManagerRef = "pgTransactionManager"		// transactionManager의 이름
)
@MapperScan(    // mybatis
        sqlSessionFactoryRef = "sqlSessionFactory",
        // mapper 클래스 위치
        basePackages = {"mapper 클래스 위치", "여러개를", "적을수도", "있음"}   
)
public class PostgresDataSourceConfiguration {

 

2. 데이터 소스 설정

  • dataSourceProperties(): DataSourceProperties 빈을 생성
    • spring.postgres.datasource로 시작하는 설정을 사용
  • dataSource(): HikariDataSource 빈을 생성
    • spring.postgres.datasource.hikari로 시작하는 설정을 사용
    @Primary
    @Bean
    @ConfigurationProperties("spring.postgres.datasource")
    public DataSourceProperties dataSourceProperties() {    // datasource 관련 설정
        return new DataSourceProperties();
    }

    @Primary
    @Bean
    @ConfigurationProperties("spring.postgres.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {   // hikari 커넥션 풀 관련 설정
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

 

3. JPA 설정

  • pgEntityManagerFactory(): JPA 엔티티 매니저 팩토리 빈을 생성
    • packages: JPA 엔티티 클래스가 위치한 패키지를 지정
  • pgTransactionManager(): JPA 트랜잭션 매니저 빈을 생성
    @Primary
    @Bean(name = "pgEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean pgEntityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                         DataSource dataSource,
                                                                         HibernateProperties hibernateProperties,
                                                                         ConfigurableListableBeanFactory beanFactory) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = builder
                .dataSource(dataSource)
                .packages("entity 위치" // entity 위치
                        , "여러개"
                        , "작성"
                        , "가능")
                .properties(hibernateProperties.determineHibernateProperties(
                        new HashMap<>(),
                        new HibernateSettings()))
                .build();
        entityManagerFactoryBean.getJpaPropertyMap().put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
        return entityManagerFactoryBean;
    }

    @Primary
    @Bean(name = "pgTransactionManager")
    public PlatformTransactionManager pgTransactionManager(EntityManagerFactory pgEntityManagerFactory) {
        return new JpaTransactionManager(pgEntityManagerFactory);
    }

 

4. MyBatis 설정

  • sqlSessionFactory(): MyBatis SQL 세션 팩토리 빈을 생성
    • setMapperLocations: MyBatis 매퍼 XML 파일의 위치를 지정
  • sqlSessionTemplate(): MyBatis SQL 세션 템플릿 빈을 생성
    @Primary
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(applicationContext.getResources("classpath:mybatis/postgres/**/*.xml"));	// xml 위치
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Primary
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);

    }

 

전체 코드는 아래와 같다

@Configuration
@EnableJpaAuditing
@EnableJpaRepositories( // jpa
        // repository 위치
        basePackages = {"repository 위치", "여러개를", "적을수도", "있음"},
        entityManagerFactoryRef = "pgEntityManagerFactory",	// entityManagerFactory의 이름
        transactionManagerRef = "pgTransactionManager"		// transactionManager의 이름
)
@MapperScan(    // mybatis
        sqlSessionFactoryRef = "sqlSessionFactory",
        // mapper 클래스 위치
        basePackages = {"mapper 클래스 위치", "여러개를", "적을수도", "있음"}   
)
public class PostgresDataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties("spring.postgres.datasource")
    public DataSourceProperties dataSourceProperties() {    // datasource 관련 설정
        return new DataSourceProperties();
    }

    @Primary
    @Bean
    @ConfigurationProperties("spring.postgres.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {   // hikari 커넥션 풀 관련 설정
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }


    /////////////////////// jpa ///////////////////////
    @Primary
    @Bean(name = "pgEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean pgEntityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                         DataSource dataSource,
                                                                         HibernateProperties hibernateProperties,
                                                                         ConfigurableListableBeanFactory beanFactory) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = builder
                .dataSource(dataSource)
                .packages("entity 위치" // entity 위치
                        , "여러개"
                        , "작성"
                        , "가능")
                .properties(hibernateProperties.determineHibernateProperties(
                        new HashMap<>(),
                        new HibernateSettings()))
                .build();
        entityManagerFactoryBean.getJpaPropertyMap().put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
        return entityManagerFactoryBean;
    }

    @Primary
    @Bean(name = "pgTransactionManager")
    public PlatformTransactionManager pgTransactionManager(EntityManagerFactory pgEntityManagerFactory) {
        return new JpaTransactionManager(pgEntityManagerFactory);
    }

    /////////////////////// mybatis ///////////////////////
    @Primary
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(applicationContext.getResources("classpath:mybatis/postgres/**/*.xml"));	// xml 위치
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Primary
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);

    }

}

위 설정을 완료하고 기존 기능들이 잘 동작하는지 확인해보자.

 

3-2. Clickhouse Configuration

Postgres 수동 설정이 정상적으로 끝났다면 이제 ClickHouse 설정을 해보자. ClickHouse에서는 JPA를 사용하지 않기 때문에 MyBatis에 대한 설정만 해주면 된다.

대부분의 설정이 Postgres 설정과 같고, 자바 클래스에서는 카멜케이스를 사용하고 있어 아래 설정을 추가해줬다.
configuration.setMapUnderscoreToCamelCase(true) : 데이터베이스의 언더스코어(_)가 포함된 컬럼명을 자바의 카멜 케이스(camelCase) 필드명으로 자동 변환하도록 설정.

@Configuration
@MapperScan(
        sqlSessionFactoryRef = "chSqlSessionFactory",
        basePackages = {"com.company.mapper.clickhouse"}   // mapper 클래스 위치
)
public class ClickhouseDataSource {

    @Bean(name = "chDataSourceProperties")
    @ConfigurationProperties("spring.clickhouse.datasource")
    public DataSourceProperties chDataSourceProperties() {    // datasource 관련 설정
        return new DataSourceProperties();
    }

    @Bean(name = "chDataSource")
    @ConfigurationProperties("spring.clickhouse.datasource.hikari")
    public HikariDataSource chDataSource(@Qualifier("chDataSourceProperties") DataSourceProperties properties) {   // hikari 커넥션 풀 관련 설정
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    /////////////////////// mybatis ///////////////////////
    @Bean(name = "chSqlSessionFactory")
    public SqlSessionFactory sessionFactory(@Qualifier("chDataSource") DataSource datasource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(applicationContext.getResources("classpath:mybatis/clickhouse/**/*.xml"));  // xml 위치 설정
        bean.setDataSource(datasource);

        // MyBatis Configuration 생성
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true); // underscore를 camelCase로 변환 설정
        bean.setConfiguration(configuration); // 설정 적용

        return bean.getObject();
    }

    @Bean(name = "chSessionTemplate")
    public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("chSqlSessionFactory") SqlSessionFactory chSqlSessionFactory) {
        return new SqlSessionTemplate(chSqlSessionFactory);
    }

}

 

4. clickhouse 쿼리 실행

여기까지 설정이 끝났다면 거의 다 끝이다! 이제 쿼리를 직접 날려보자. 사용 방법은 기존 MyBatis 사용 방법과 동일하고, 인터페이스와 XML의 위치만 Configuration에서 설정해준 위치로 맞춰주면 된다.

아래는 쿼리 예시이다

 


설정하면서 잘 안됐던 부분

문제 1. clickhouse mybatis mapper 를 못찾는 문제

→ 잘못된 설정(Postgres)을 연결하고 있었다. ClickHouse 설정의 경우 아래와 같이 @Qualifier 키워드로 명시적으로 어떤 빈을 주입받을지 설정해줬다.

 

문제2. yml 에 설정한 커넥션 풀 개수가 적용되지 않음

기존에는 URL 관련 설정만 있는 dataSourceProperties()만 정의했었다. 쿼리의 실행은 정상적으로 됐는데 문제는 yml에 설정한 커넥션 풀 개수가 제대로 적용되지 않았다.

확인해보니 dataSourceProperties() 설정은 spring.postgres.datasource에 있는 설정만 가지고 와서 그 하위에 있는 spring.postgres.datasource.hikari 설정은 불러오지 못한 것이었다. 그래서 데이터소스 설정을 먼저 한 후 그 설정을 받아 추가로 Hikari에 대한 설정을 해주는 방식으로 해결했다.

// as-is
//    @Primary
//    @Bean
//    @ConfigurationProperties(prefix = "spring.postgres.datasource") 	// application.properties에서 사용한 이름
//    public DataSource dataSource() {
//        return DataSourceBuilder.create().build();
//    }

// to-be
    @Bean
    @Primary
    @ConfigurationProperties("spring.postgres.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Primary
    @Bean
    @ConfigurationProperties("spring.postgres.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

 


사실 이전에 멀티 데이터소스 설정 내용을 포스팅한 적이 있는데, 그때는 스프링을 해본 지 한 달도 안 된 때여서 깊이 이해하지 못했던 것 같다. 이번 기회에 다시 설정해보면서 조금 더 이해할 수 있었다. 3년 전과 지금을 비교해보면 당시에는 MyBatis의 비중이 컸지만, 이제는 대부분의 쿼리를 JPA로 전환 완료했다. 또한, JPA 엔티티의 위치도 중구난방이었는데 하나의 패키지로 정리했고, 멀티 프로젝트를 도입하는 등 그때보다 우리 프로젝트가 많이 성장한 것 같아 뿌듯하다.

https://rangerang.tistory.com/70

 

[spring boot] mybatis + jpa multi datasource 설정하기

스프링 부트 프로젝트를 개발하며 2개의 데이터베이스를 연결해야하는 이슈가 생겼다. 기존 프로젝트는 mybatis와 jpa를 섞어서 사용하는 구조이고, multi datasource를 설정하기 위해서는 수동설정이

rangerang.tistory.com

참고

https://clickhouse.com/docs/en/integrations/java
https://jsijsi99.tistory.com/9
https://velog.io/@ghkvud2/Multiple-DataSource-적용하기
https://velog.io/@dongvelop/Spring-Boot-Hikari-CP-커스텀으로-성능-최적화하기
https://github.com/ClickHouse/clickhouse-java
https://github.com/lewis-ing/clickhouse-native/tree/master/src
https://clickhouse.com/docs/en/interfaces/jdbc
댓글