티스토리 뷰

스프링 부트 프로젝트를 개발하며 2개의 데이터베이스를 연결해야하는 이슈가 생겼다. 기존 프로젝트는 mybatis와 jpa를 섞어서 사용하는 구조이고, multi datasource를 설정하기 위해서는 수동설정이 필요했다. 수동설정을 위해 mybatis는 mapper가 어디에 있는지, jpa는 entity와 repository가 어디에 있는지를 모두 설정해주어야 했는데 기존 프로젝트의 구조가 이곳저곳에 퍼져있어 어려움을 겪었다.

1. application.properties에 datasource 입력

application.properties에 두개의 데이터베이스에 접근하기 위한 정보를 입력해준다. 여기서 중요한 점은 url이 아닌 jdbc-url을 사용해야한다는 것이다. 그 이유는 spring boot 2.0부터 기본으로 사용하는 커넥션 풀이 HikariCP로 변경되었는데 HikariCP에선 databaseURL 설정에서 정의된 변수가 url 이 아닌 jdbcUrl로 정의되어 있기 때문이다.

그럼 단일 데이터베이스를 사용할 때에는 왜 url을 사용하는지 의문이 생길 수 있다. 그 이유는 단일 connection의 경우 자동으로 url을 jdbc-url로 인식하여 주입해주지만, 두개이상의 datasource를 가지는 경우 수동설정이 필요하기 때문에 url을 jdbc-url로 인식하지 못한다.

## application.properties
# primary db
spring.primary.datasource.driver-class-name=#{driver-class-name}
spring.primary.datasource.jdbc-url=#{jdbc-url}
spring.primary.datasource.username=#{username}
spring.primary.datasource.password=#{비밀번호}

# secondary db
spring.secondary.datasource.driver-class-name=#{driver-class-name}
spring.secondary.datasource.jdbc-url=#{jdbc-url}
spring.secondary.datasource.username=#{username}
spring.secondary.datasource.password=#{비밀번호}

 

2. DataSource 설정

이제 위에서 설정한 데이터베이스에 접근하기 위한 datasource를 지정해주는 파일을 만들어보자.

설정 클래스에서 설정해줘야할 값


Datasource

순수 jdbc로 데이터베이스에 접근을 하면, 데이터베이스에 접근할 때마다 connection을 맺고 끊는 작업을 한다. 이 connection을 맺고 끊는 작업을 줄이기 위해 미리 connection을 생성해 두고, 데이터베이스에 접근하고자 하는 사용자에게 미리 생성된 connection을 제공하고 돌려받는다. 이 connection들을 모아두는 장소를 
connection pool이라 하며, Datasource는 java 에서 connection pool을 지원하기 위한 인터페이스이다.

[jpa]

EntityManagerFactory : EntityManager를 찍어내는 역할

EntityManager는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드간에 절대 공유하면 안된다. 따라서 엔터티매니저는 상황에 따라 계속 만들어줘야 하는데 이 일을 EntityManagerFactory가 수행한다.

(참고) EntityManager

엔터티를 관리하는 역할을 수행하는 클래스이다. 엔티티 매니저 내부에 **영속성(비휘발성) 컨텍스트(Persistence Context)**라는 걸 두어서 엔티티들을 관리한다.

PlatformTransactionManager

트랜잭션 관리자

 

[mybatis]

SqlSessionFactory : SqlSession을 찍어내는 역할

DataSource를 참조하여 MyBatis와 Mysql 서버를 연동시켜준다. SqlSession을 생성하기 위해 사용한다.

(참고) SqlSession

sql문을 실제 호출해주는 역할

SqlSessionTemplate

SqlSession을 구현하고 코드에서 SqlSession를 대체하는 역할을 한다. SqlSessionTemplate 은 쓰레드에 안전하고 여러개의 DAO나 mapper에서 공유할수 있다. SqlSessionTemplate은 SqlSession이 현재의 스프링 트랜잭션에서 사용될수 있도록 보장한다. 추가적으로 SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다. 또한 마이바티스 예외를 스프링의 DataAccessException로 변환하는 작업또한 처리한다.


 

2-1. Primary DataSource 

## PrimaryDataSource
## import 생략

@Configuration
@PropertySource({"classpath:application.properties"})
@EnableJpaRepositories(
        basePackages = {"jpa : repository 위치", "여러개라면 배열로"},
        entityManagerFactoryRef = "entityManagerFactory",	// entityManagerFactory의 이름
        transactionManagerRef = "transactionManager"		// transactionManager의 이름
)

@MapperScan(
        sqlSessionTemplateRef = "sqlSessionTemplate",
        basePackages = {"mybatis : mapper 위치", "여러개라면 배열로"}  
)

public class PrimaryDataSource {

    @Autowired
    private Environment env;

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


    /////////////////////// jpa ////////////////////
    @Primary
    @Bean
    public EntityManagerFactory entityManagerFactory(DataSource dataSource) {
        final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("jpa : entity 위치", "여러개선언 가능");
        final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        final HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName()); // 네이밍
        properties.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName()); // 네이밍
        em.setJpaPropertyMap(properties);
        em.afterPropertiesSet();

        return em.getObject();
    }

    @Primary
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }

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

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

    }

}

 

주의해서 설정할 부분

1. @Primary : default로 사용하고자 하는 Datasource를 @Primary로 지정해준다. 만약 지정해주지 않는다면 우선적으로 실행해야할 Datasource 를 지정해주지 못해 오류가 발생할 수 있다.

2. @EnableJpaRepositories

- basePackages : jpa repository가 들어있는 패키지

3. @MapperScan

- basePackages : mapper 패키지

4. entityManagerFactory

- setPackagesToScan : jpa entity 패키지

- 네이밍 : 카멜케이스를 언더스코어로 변경해주도록 설정(기존 설정->hibernate의 버전에 따라 디폴트값이 다름)

5. sessionFactory

- setMapperLocations : mapper의 xml이 위치한 패키지

 

2-2. Secondary Datasource

두번째 데이터소스를 설정해보자. 기존프로젝트에서 mybatis와 jpa를 섞어서 사용한 이유가 mybatis에서 jpa로 변화하는 과도기 였기 때문이었다. 따라서 두번째 데이터소스에서는 mybatis관련 설정은 제외했다. 대부분의 내용은 primary와 동일하지만 @Primary 어노테이션을 삭제해야한다.

@Configuration
@PropertySource({"classpath:application.properties"})
@EnableJpaRepositories(
        basePackages = {jpa : repository 패키지},  // 
        entityManagerFactoryRef = "secondaryEntityManagerFactory",
        transactionManagerRef = "secondaryTransactionManager"
)

@ConfigurationProperties(prefix = "spring.secondary.datasource")
public class SecondaryDataSource extends HikariConfig {

    @Autowired
    private Environment env;

    @Bean(name = "SecondaryDataSource")
    @ConfigurationProperties(prefix = "spring.secondary.datasource")
    public DataSource secondaryDataSource() {
        /*
        * LazyConnectionDataSourceProxy를 사용하면 트랜잭션이 시작 되더라도 실제로 커넥션이 필요한 경우에만 데이터소스에서 커넥션을 반환한다.
        * */
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(env.getProperty("spring.secondary.datasource.jdbc-url"));
        dataSource.setUsername(env.getProperty("spring.secondary.datasource.username"));
        dataSource.setPassword(env.getProperty("spring.secondary.datasource.password"));
        dataSource.setDriverClassName(env.getProperty("spring.secondary.datasource.driver-class-name"));
//        dataSource.setMinimumIdle(0);         // 최소 커넥션 개수
//        dataSource.setIdleTimeout(10);        // 유지시간
        return new LazyConnectionDataSourceProxy(dataSource);
    }

    //////////////// jpa ////////////////////////
    @Bean(name ="secondaryEntityManagerFactory")
    public EntityManagerFactory  secondaryEntityManagerFactory(@Qualifier("SecondaryDataSource") DataSource dataSource) {
        final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setJpaVendorAdapter(vendorAdapter);
        em.setPackagesToScan(
               jpa : entity 패키지
        );

        final HashMap<String, Object> properties = new HashMap<>();
        properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName()); // 네이밍
        properties.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName()); // 네이밍

        em.setJpaPropertyMap(properties);
        em.afterPropertiesSet();

        return em.getObject();
    }

    @Bean(name = "secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);

        return tm;
    }

}

여기서는 primary와 다르게 LazyConnectionDataSourceProxy 를 사용했다. 이유는 스프링에서는 트랜잭션 시작 시 실제 사용여부와 무관하게 커넥션을 확보한다는 단점을 가지고 있는데, LazyConnectionDataSourceProxy를 사용하면 트랜잭션이 시작 되더라도 실제로 커넥션이 필요한 경우에만 데이터소스에서 커넥션을 반환한다.

 

3. 사용

실제 사용은 mybatis와 jpa 모두 경로를 잘 설정해줬다면 해당 경로에서 특별한 설정을 해줄필요없이 사용 가능하다. 하지만 예외적으로 QueryDSL을 사용하는 경우는 에러가 나서 찾아보니 QueryDSL을 사용하게 될 경우 기존에는 QuerydslRepositorySupport을 상속받아 사용했는데, 이렇게 되면 2개의 entitymanagerfactory 중 어떤 것을 사용해야 할지 몰라 에러를 내게 된다고 한다. 따라서 어떤 entitymanagerfactory를 사용할지 정해줘야한다.

해당내용은 아래 블로그를 참조하자.

www.4te.co.kr/892

 

 

[References]
https://deepweller.tistory.com/6
https://sanghye.tistory.com/26
http://kwon37xi.egloos.com/m/5364167
https://growing-up-constantly.tistory.com/48#recentEntries
https://eternalteach.tistory.com/67
https://www.baeldung.com/spring-data-jpa-multiple-databases
perfectacle.github.io/2018/01/14/jpa-entity-manager-factory/
https://velog.io/@shson/mybatis-sqlSession-%EC%82%AC%EC%9A%A9

https://do-study.tistory.com/97
댓글