이것 저것 남겨보겠습니다.
by zoooo-hs
회사에서 새로운 기능 구현에 있어 매 서비스 요청마다 다양한 DataSource 에 DB Query를 실행해야 할 일이 있었다. 제품은 Spring Boot + Spring Data JPA를 사용 중인데, 이미 단일 DataSource 에 연결되어 비즈니스 로직에 필요한 데이터들을 저장하고 불러오고 있었다. 이런 상황에서 기존의 DataSource 와 연결된 JPA Repository 및 기타 Configuration 수정 없이 새로운 기능을 구현할 방법을 찾아야 했다.
JPA Dynamic DataSource, JPA Change DataSource 등 JPA 를 사용하는 환경에서 여러 DataSource를 사용할 방법을 구글링해 보았다. 주로 나오는 글들은 HIbernate의 Multi tenancy 기능을 사용해 요청별 DataSource를 switching 하는 방법1을 소개하거나, Repository 별 다른 DataSource를 주입해 사용하는 방법2을 소개했다. 그러나 내 문제에 적용하기엔 문제가 있었다. 내 상황에 적용하지 못한 이유는
그래도 앞서 시도해본 방법들에서 얻어간 게 없진 않다. 공통으로 EntityManagerFactory를 생성할 때 DataSource를 주입하는 과정이 있었다.
@Bean
private LocalContainerEntityManagerFactoryBean productEntityManager(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource); // 핵심
em.setPackagesToScan(new String[] { "com.base.pakcage.for.entity" });
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.afterPropertiesSet();
return em;
}
이러한 EntityManagerFactory를 이용하면 EntityManager를 만들 수 있는데, 사실 EntityManager만 있으면 JPA를 쓸 수 있다. 이런 점들을 문제 상황과 조합해보면 매 서비스 요청마다 바뀌는 DataSource 정보에 맞춰 EntityManagerFactory를 만들어 EntityManager를 생성해 사용할 수 있다는 것이다.
DataSource는 다음과 같이 정의해 생성할 수 있다.
public DataSource productDataSource(String driverName, String url, String username, String password) {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(this.getClass().getClassLoader());
dataSourceBuilder.driverClassName(driverName)
.url(url)
.username(username)
.password(password);
return dataSourceBuilder.build();
}
이제 아래처럼 EntityManager를 생성할 수 있다.
EntityManagerFactory emf = productEntityManager(productDataSource(driverName, url, username, password))
.getNativeEntityManagerFactory();
EntityManager em = emf.createEntityManager();
이젠 자유롭게 EntityManager를 통해 원하는 Entity의 영속성을 불러오고 저장하고 지지고 볶을 수 있다.
SomeEntity someEntity = em.find(SomoeEntity.class, 1L);
Spring Data JPA의 JpaRepository가 아닌 JPA의 EntityManager로 직접 영속성 관리를 해서, persist의 경우엔 직접 transaction을 잡아줘야 한다.
someEntity.setName("another name");
try {
em.getTransaction().begin();
em.persist(someEntity);
em.getTransaction().commit();
}
catch (Exception e) {
em.getTransaction().rollback();
}
이렇게 하면 요청마다 다른 Datasource여도 Query 실행이 가능하다. 그런데 조금 아쉽다.
EntityManager 자체로도 이미 JPA를 사용할 수 있지만, 우리는 이미 Spring Data JPA에서 제공하는 JpaRepository의 꿀을 빨고 있는 상태이다. 사람이 서 있으면 앉고 싶고 앉으면 눕고 싶더라, 그래서 방법을 찾아보았다. 아래 코드3로 EntityManager를 통해 JpaRepository 인스턴스를 만들어 쓸 수 있다.
SomeRepository someRepository =
new JpaRepositoryFactory(em).getRepository(SomeRepository.class);
SomeEntity someEntity = someRepository.findById(1L);
아직 명확한 이유를 찾지 못했지만, transaction 관리가 필요한 save (persist, merge를 요구하는)와 같은 메소드의 경우 EntityManager를 사용할 때와 마찬가지로 transaction을 직접 잡아줘야 한다.
someEntity.setName("another name");
try {
em.getTransaction().begin();
someRepository.save(someEntity);
em.getTransaction().commit();
}
catch (Exception e) {
em.getTransaction().rollback();
}
이로써 JpaRepository 까지 사용 가능한 상황이 되었다.
EntityManager는 매 서비스 메소드에 만들어 사용하고 서비스 메소드가 끝나면 close 해 사용하도록 권장된다.4 EntityManager를 만들기 위해서 EntityManagerFactory를 사용하는데 같은 Datasource에 대해서는 EntityManagerFactory를 한번 만들어서 여러 번 사용해도 괜찮다.
오히려 EntityManagerFactory를 생성할 때 성능 이슈 4,5 (레퍼찾기)가 있으므로 같은 DataSource에 대해서는 하나의 EntityManagerFactory를 사용하는 게 좋다. 이를 위해 DataSource, EntityManagerFactory를 key value 쌍으로 메모리에 들고 있으면 중복하여 다시 생성하는 일이 없을 것이다.