모르지 않다는 것은 아는것과 다르다.

Spring Jpa

JPA에서 여러 종류의 영속성 관리하기

채마스 2023. 8. 28. 18:32

개요

  • JPA 기반의 애플리케이션에서 여러 개의 DB를 사용해서 개발하고 싶다는 요청이 들어왔다.
  • 그렇다는 건, 여러 종류의 영속성을 사용하고 싶다는 의미다.
  • 이 요구사항을 해결하기 위해서 여러 개의 Persistence-Unit을 정의하고, 그에 따라 여러 개의 EntityManagerFactory를 정의하는 과정을 진행했다.
  • 이 과정에서 새로 알게된 내용들이 많아서 간단하게나마 정리해 보려고 한다.

 

먼저 JPA에서는 영속성을 관리하는 개념에 대해서 정리하고 넘어가자.

 

JPA에서 영속성을 제어하기 위해서 필요한 구성요소

  • JPA에서 영속성을 관리하기 위해서는 위와 같은 구성요소가 세팅이 되어있어야 한다.
  • 스프링부트를 사용하면 자동적으로 위와 같은 구성요소가 세팅이 된다.
  • 구성요소 각각에 대해서 정리해 보자.

 

PersistenceUnit

  • 데이터베이스 연결에 대한 설정 및 관리를 포함하는 논리적인 단위다.
  • 즉, 각 데이터베이스마다 별도의 퍼시스턴스 유닛을 정의해야 한다.
  • 데이터베이스 연결, 엔티티 클래스, JPA 공급자에 대한 설정 등을 포함하여 PersistenceUnit를 정의한다.
  • 과거에는 persistence.xml 파일에 정의했으나 현재는 대부분 Configuration 클래스로 설정한다.

 

EntityManagerFactory

  • EntityManagerFactory는 EntityManager 인스턴스를 생성하는 팩토리 역할을 한다.
  • 일반적으로 애플리케이션 구동 시점에 EntityManger를 빈으로 등록하고 EntityManger를 재사용한다. (여러 번 EntityManager를 생성하지 않는다.)

 

EntityManager

  • EntityManager의 구현체는 기본적으로 PersistenceContext(영속성 컨텍스트)를 가지고 있다.
  • 그렇기 때문에 엔티티 객체들의 생명주기를 관리한다.

 

TransactionManager

  • TransactionManager는 데이터베이스 트랜잭션의 생명주기를 관리한다.
  • JPA를 사용하는 경우, JpaTransactionManager가 빈으로 등록되며, JpaTransactionManager에서 트랜잭션이 열리는 시점에 EntityManager를 통해서 영속성 컨텍스트를 생성한다.

 

DataSource

  • DataSource는 커넥션을 관리하는 역할을 한다.
  • 즉, DataSource는 커넥션 풀이라고 생각하면 된다.
  • 스프링부트를 사용하면 기본적으로 HikariDataSource가 빈으로 등록된다.

 

위에서 설명한 구성요소는 스프링부트를 사용하면 Auto Copfiguration에 의해서 자동으로 빈으로 등록된다. 그 과정에 대해서 간단히 알아보자.

 

HibernateJpaConfiguration

  • 정리에 앞서, HibernateJpaConfiguration에 대해서 알아보자.
  • 스프링부트를 사용하면, application.yml를 참고해서 스프링부트가 알아서 DataSoure, EntityManagerFactory, TransactionManager를 빈으로 등록해 준다.
  • 그중에서 EntityManagerFactory, TransactionManager는 HibernateJpaConfiguration가 빈으로 등록한다.

    • 먼저, @ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class }) 를 통해서 Hibernate JPA와 관련된 라이브러리가 존재하지 않을 경우에는 이 설정이 적용된다.

@ConditionalOnClass에서 SessionImplementor를 포함시킨 이유는 Hibernate를 사용하는 경우에만 이 설정이 적용되도록 하기 위함이다.

  • @EnableConfigurationProperties(JpaProperties.class) 를 통해서 application.yml에 있는 jpa 관련 설정을 읽어와서 JpaProperties 설정을 빈으로 등록한다.
  • @AutoConfigureAfter({ DataSourceAutoConfiguration.class })를 통해서 application.yml에 있는 DataSource 설정을 읽어와 빈으로 등록한 다음에 HibernateJpaConfiguration가 활성화된다.
  • @Import(HibernateJpaConfiguration.class)가 HibernateJpaConfiguration에서 가장 중요한 부부인데, @Import를 통해서 HibernateJpaConfiguration설정을 가져오고, HibernateJpaConfiguration는 JpaBaseConfiguration를 상속받고 있다.

  • 위와 같이 JpaBaseConfiguration가 EntityManagerFactory와 TransactionManager를 빈으로 등록해 주는 것을 알 수 있다.
    • 또한, @ConditionalOnMissingBean 가 붙어있기 때문에, 개발자가 직접 EntityManagerFactory와 TransactionManager를 빈으로 등록한다면, 개발자가 등록한 빈이 우선권을 가진다는 것도 확인할 수 있다.

 

JpaRepositoriesAutoConfiguration

  • 이제 본격적으로 JPA 관련 설정에 대해서 알아보자.

  • JpaRepositoriesAutoConfiguration는 Spring Data JPA와 관련이 있는 설정 클래스이다.
  • @AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class }) 에서 HibernateJpaAutoConfiguration 이후에 활성화되는 것을 확인할 수 있다.
  • JPA를 사용하면, 보통 아래와 같이 JpaRepository를 상속해서 Repository를 만들 것이다.

  • 나는 JPA를 처음 사용할 때, 어떻게 메소드만 인터페이스만 정의하고도 메소드를 사용할 수 있지?라는 의문이 있었다.
  • 그걸 가능하게 해주는 것이 바로 JpaRepositoriesAutoConfiguration의 자동설정이라는 것을 알게 되었다.
  • 다시 말해, 개발자는 findByTempIdAndTempNm() 메소드만 정의하면, JpaRepositoriesAutoConfiguration의 자동설정을 통해서 JpaRepository의 구현체를 JPA에서 알아서 만들어주는 것이다.

 

위에서 영속성과 관련된 구성요소에 대해서 알아보았다. 그러면 영속성을 여러 개로 관리하려면 어떻게 하면 될까?

 

하나 이상의 영속성 관리하기

만약 primary_db, extra_db 이렇게 두 개의 데이터베이스를 사용해서 영속성을 관리한다고 가정하자. 그렇다면 구조는 아래와 같다.

  • 위와 같이, primary, extra 각각 PersistenceUnit, EntityManagerFactory, EntityManager, TransactionManager, DataSource를 구성해 줘야 한다.

 

이제 본격적으로 구현에 들어가 보자. 먼저 primary db에 해당하는 PrimaryPersistenceConfig부터 구현해 보자.

 

PrimaryPersistenceConfig 구현

  • 먼저 primary_db에 대한 구현부터 시작해 보자.

 

@EnableJpaRepositories 적용

  • Jpa 관련 Configuration을 정의하기 위해서는 아래와 같이 @EnableJpaRepositories를 붙여야 한다.

  • @EnableJpaRepositories는 Spring Data JPA 리포지토리를 설정하고 활성화하는 데 사용되는 어노테이션이다.
  • @EnableJpaRepositories를 사용해서 JpaRepositoryAutoConfiguration에서 제공하는 기본 설정을 재정의하거나 추가 설정할 수 있다.
  • @EnableJpaRepositories의 속성 중에서 내가 사용한 속성은 아래와 같이 5가지다.
    • basePackages: 리포지토리 인터페이스를 스캔할 패키지를 지정한다. JpaRepositoryAutoConfiguration에서 basePackages정보를 바탕으로 해당 패키지에서 리포지토리 인터페이스를 찾아 구현체를 생성한다.
    • entityManagerFactoryRef: JpaRepositoryAutoConfiguration은 이 설정을 인식하고 해당 EntityManagerFactory를 사용하여 리포지토리 구현체를 생성한다.
    • transactionManagerRef: JpaRepositoryAutoConfiguration은 이 설정을 인식하고 해당 PlatformTransactionManager를 사용하여 트랜잭션 관리를 수행한다.
    • excludeFilters: basePackages를 기준으로 조회된 레포지토리 중에서 excludeFilters에 정의한 클래스를 제외한다.
  • 위 4가지 속성 중에서 excludeFilters를 통해서 extra_db에 해당되는 레포지토리를 제외시켜 줘야 한다.
  • excludeFilters에서 사용할 애노테이션을 아래와 같이 구현했다. 그리고 레포지토리에 붙여주었다.

  • 위와 같이 설정하면 JpaRepositoryAutoConfiguration은에 의해서 basePackages에서 설정한 패키지 하위에서 JpaRepository를 상속한 Repository를 스캔한 뒤, excludeFilters에 의해서 필터링한다.

  • 이제 필터링된 레포지토리에 대해서 구현체를 만드는데, 그 과정에서 아래와 같이 entityManagerFactoryRef에서 설정한 entityManagerFactory를 통해서 만들어진 EntityManager를 JpaRepositoryFactory에게 주입해 준다.

  • 그다음, JpaRepositoryFactory의 getTargetRepositor() 메소드에서 JpaRepository에 대한 구현체를 만들어준다.

 

PrimaryPersistenceUnit, PrimaryEntityManagerFactory 구성

  • 이전에 HibernateJpaConfiguration에 대해서 설명하면서 @ConditionalOnMissingBean 가 붙어있기 때문에, 개발자가 직접 EntityManagerFactory와 TransactionManager를 빈으로 등록한다면, 개발자가 등록한 빈이 우선권을 가진다고 언급했다.
  • Jpa에서 여러 개의 DataSource를 사용하기 위해서는 JPA 설정을 직접 커스텀해야 된다. 이때 사용되는 것이 LocalContainerEntityManagerFactoryBean이다.
  • LocalContainerEntityManagerFactoryBean는 JPA를 사용할 때, EntityManagerFactory 인스턴스를 생성해 주는 스프링의 FactoryBean 구현체다.
  • LocalContainerEntityManagerFactoryBean는 EntityManagerFactory를 손쉽게 빈으로 등록할 수 있게 해 준다.
  • 아래 코드를 보면 entityManagerFactory를 등록하는데 반환 타입이 LocalContainerEntityManagerFactoryBean인 것을 확인할 수 있다.

  • LocalContainerEntityManagerFactoryBean의 생성을 도와주는 EntityManagerFactoryBuilder에서 주로 사용하는 API에 대해서 알아보자.
  • persistenceUnit()
    • 영속성 유닛(Persistence Unit)의 이름을 설정하는 데 사용된다. 영속성 유닛은 JPA의 설정 단위로, 엔티티 매니저 팩토리를 생성하는 데 필요한 정보를 제공하는 데 사용된다.
  • packages()
    • 이 메소드는 엔티티 클래스들이 포함된 패키지의 이름을 받아들입니다. JPA 엔티티 클래스를 찾고 스캔하려는 패키지를 지정하는 데 사용된다.
    • 현재는 PrimaryPersistenceConfig를 구성하는 중이기 때문에 Extra에 해당되는 엔티티는 제외시켜주어야 한다.
    • 나는 그러기 위해서 아래와 같이 EntitySearchHelper와 @ExtraEntity를 구현했다.

  • 위와 같이 설정하면 아래와 같이 @ExtraEntity가 붙은 엔티티를 제외한 모든 엔티티의 패키지 경로가 반환된다.
  • 이제 PRIMARY_PERSISTENCE_UNIT라는 PersistenceUnit이 관리할 엔티티 목록에 대한 설정이 완료되었다.

 

PrimaryDataSource 구성

  • DataSource를 구성하기 위해서 나는 @ConfigurationProperties를 사용했다. (@ConfigurationProperties에 대한 내용은 이번 글의 범위를 벗어나니 생략하도록 하자.)

  • 또한, DataSourceProperties에 HikariCP 설정을 추가하기 위해서 BaseDataSourceProperties를 구성했다.
  • 이 과정에서 DataSourceProperties를 직접 상속받으면 빈 충돌에러가 발생해서 DataSourceProperties 소스를 그대로 가져와서 CustomDataSourceProperties를 만들어서 사용했다.

 

PrimaryTransactionManager 구성

이제 마지막으로 TransactionManager를 구성해 보자.

  • TransactionManager는 아주 간단하다. EntityManagerFactory만 의존 주입을 해주면 된다.

 

다음으로 extra db에 해당하는 ExtraPersistenceConfig를 구성해 보자.

 

ExtraPersistenceConfig 구성

PrimaryPersistenceConfig에서 설명한 내용은 제외하고 ExtraPersistenceConfig를 구성해 보자.

 

@EnableJpaRepositories 적용

  • basePackages는 PrimaryPersistenceConfig와 동일하게 설정해 주었다.
  • 하지만 PrimaryPersistenceConfig와는 다르게 excludeFilters가 아닌 includeFilters가 사용되었다.
  • 그렇기 때문에 ExtraRepository가 붙은 레포지토리들만 포함시키게 된다.

 

ExtraPersistenceUnit, ExtraEntityManagerFactory 구성

  • 이번에는 EntitySearchHelper에서 @ExtraEntity 애노테이션이 붙은 엔티티의 패키지정보를 가져오고 있다.
  • 이제 EXTRA_PERSISTENCE_UNIT라는 PersistenceUnit이 관리할 엔티티 목록에 대한 설정이 완료되었다.

 

ExtraDataSource 구성

  • 위와 같이 yml파일에 prefix만 맞춰 주면 @ConfigurationProperties를 사용할 수 있다.

 

ExtraTransactionManager 구성

  • TransactionManager도 위와 같이 EntityManagerFactory를 주입해 주어 만들었다.

 

왜 EntityManager는 빈으로 등록하지 않는가? EntityManager는 따로 빈으로 등록하지 않아도 자동으로 등록된다.

 

EntityManager 자동 등록

  • 위와 같이 EntityManager를 빈으로 등록하지 않아도 자동으로 등록되는 것을 확인할 수 없다.
  • 오히려 EntityManger를 빈으로 등록하게 되면 예상치 못한 에러가 날 수 있다. (예를 들면, 커넥션이 누수된다던가...)
  • 이 부분은 꼭 주의하도록 하자.

 

사용방법 정리

  • PrimaryPersistenceConfig와 ExtraPersistenceConfig에 대한 설정이 끝났으니 실제로 개발하는 방법을 정리해 보자.
  • 위에서도 언급했지만 아래와 같이 extra_db에 해당되는 엔티티와 레포지토리에 @ExtraEntity, @ExtraRepository를 달아주면 된다.

  • 또한 @Transactional 사용 시 Extra에 해당하는 경우 아래와 같이 value 속성으로 extraTransactionManager의 빈이름을 넣어줘야 한다.

  • 만약 value 속성을 넣어주지 않으면 @Transactional을 @Primary로 등록된 TransactionManager로 동작하기 때문이다.

 

서로 다른 영속성을 하나의 트랜잭션으로 관리할 수 있을까? 스프링은 고맙게도 ChainedTransactionManager를 통해서 이 기능 또한 지원해 준다.

 

ChainedTransactionManager

Spring에서는 ChainedTransactionManager를 통해서 서로 다른 영속성을 하나의 트랜잭션으로 관리할 수 있다. 즉, 다른 DB의 트랜잭션을 묶어서 처리할 수 있는 것이다.

  • 위와 같이 현재 스프링 컨테이너 등록된 PlatformTransactionManager(트랜잭션 매니저)들을 묶어서 ChainedTransactionManager를 만들 수 있다.
  • 나는 스프링 컨테이너등록된 모든 트랜잭션 매니저를 묶었지만, 원하는 트랜잭션매니저만 선택해서 묶어서 ChainedTransactionManager를 만들 수도 있다.
  • 이제 아래와 같이, 위에서 등록한 트랜잭션매니저를 사용하면 된다.

  • 위와 같이 @Transactional의 속성값으로 좀 전에 빈으로 등록한 ChainedTransactionManager 타입의 트랜잭션 매니저의 이름을 넘겨주면 된다.
  • 이렇게 되면 서로 다은 데이터소스의 로직을 하나의 트랜잭션으로 관리할 수 있다.



소스 코드

마지막으로 전체 소스 코드를 정리해 보자. 구현한 클래스는 아래와 같다.

  • BaseDataSourceProperties
  • CustomDataSourceProperties
  • PrimaryDataSourceProperties
  • ExtraDataSourceProperties
  • ExtraEntity
  • ExtraRepository
  • EntitySearchHelper
  • PrimaryPersistenceConfig
  • ExtraPersistenceConfig
  • ChainedTransactionConfig

 

BaseDataSourceProperties


import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Getter;
import lombok.Setter;
import test.dataSource.config.properties.custom.CustomDataSourceProperties;

import javax.sql.DataSource;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * DataSourceProperties에 추가적으로 HikariCP 설정을 추가
 */
@Getter
@Setter
public class BaseDataSourceProperties extends CustomDataSourceProperties {

    private HikariConfig hikari;

    public DataSource createDataSource() {
        ParsedURL parsedURL = new ParsedURL(getUrl());

        hikari.setPoolName(parsedURL.getPoolName());
        hikari.setJdbcUrl(parsedURL.getUrl());
        hikari.setUsername(getUsername());
        hikari.setPassword(getPassword());
        hikari.setDriverClassName(getDriverClassName());

        return new HikariDataSource(hikari);
    }

    @Getter
    public static class ParsedURL {
        private final Pattern DB_NAME_PATTERN = Pattern.compile("jdbc:.+://[^/]+/([^?]+)");

        private final String url;
        private final String poolName;

        public ParsedURL(String url) {
            this.url = url;
            this.poolName = getMatchString(url, DB_NAME_PATTERN);
        }

        private String getMatchString(String url, Pattern pattern) {
            Matcher matcher = pattern.matcher(url);
            if (matcher.find() && matcher.groupCount() > 0) {
                return matcher.group(1);
            } else {
                return "defaultPool";
            }
        }
    }
}

 

CustomDataSourceProperties


/**
 * DataSourceProperties를 그대로 가져온 클래스 {@link DataSourceProperties}
 * <p>
 * DataSourceProperties 사용하게 되면 DataSourceProperties 타입의 빈이 여러개라서 빈 충롤 에러가 발생합니다.
 * 위 문제를 해결하기 위해서 DataSourceProperties 코드를 그대로 복사해서 새로운 클래스를 구성하였습니다.
 */
@Primary
public class CustomDataSourceProperties implements BeanClassLoaderAware, InitializingBean {
  //DataSourceProperties 내용과 동일...
}
  • 구현 내용은 DataSourceProperties와 동일하다.

 

PrimaryDataSourceProperties

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import test.dataSource.config.properties.base.BaseDataSourceProperties;

/**
 * Primary DataSource Properties
 */
@Configuration
@ConfigurationProperties("spring.datasource")
public class PrimaryDataSourceProperties extends BaseDataSourceProperties {
}

 

ExtraDataSourceProperties

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import test.dataSource.config.properties.base.BaseDataSourceProperties;

/**
 * Extra DataSource Properties
 */
@Configuration
@ConfigurationProperties("extra.datasource")
public class ExtraDataSourceProperties extends BaseDataSourceProperties {
}

 

ExtraEntity

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 해당 엔티티가 Extra PersistenceUnit 참조함을 의미하는 애노테이션
 */
@Target(TYPE)
@Retention(RUNTIME)
public @interface ExtraEntity {

    /**
     * DataSource Configuration Prefix
     */
    String prefix() default "";

}

 

ExtraRepository

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 해당 레포지토리가 Extra PersistenceUnit 참조함을 의미하는 애노테이션
 */
@Target(TYPE)
@Retention(RUNTIME)
public @interface ExtraRepository {

    /**
     * DataSource Configuration Prefix
     */
    String prefix() default "";
}

 

EntitySearchHelper


import lombok.RequiredArgsConstructor;
import org.reflections.Reflections;
import org.springframework.stereotype.Component;
import test.dataSource.annotation.ExtraEntity;
import test.dataSource.config.PrimaryPersistenceConfig;

import javax.persistence.Entity;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 데이터 소스에 포함된 엔티티 패키지를 조회하는 Helper 클래스
 */
@Component
@RequiredArgsConstructor
public class EntitySearchHelper {

    private final Reflections reflections = new Reflections(PrimaryPersistenceConfig.BASE_PACKAGE);

    /**
     * Primary EntityManager에서 관리할 Entity 검색
     *
     * @return Primary EntityManager가 관리하는 엔티티들이 속한 패키지 목록
     */
    public Set<String> findEntityPackagesOfPrimary() {
        Set<Class<?>> primaryEntities = getEntitiesExcluding(ExtraEntity.class);
        return extractPackageNames(primaryEntities);
    }

    /**
     * Extra EntityManager에서 관리할 Entity 검색
     *
     * @param prefix 엔티티의 prefix
     * @return Extra EntityManager가 관리하는 엔티티들이 속한 패키지 목록
     */
    public Set<String> findEntityPackagesOfExtra(String prefix) {
        Set<Class<?>> extraEntities = getEntitiesMatchingPrefix(prefix);
        return extractPackageNames(extraEntities);
    }

    private Set<Class<?>> getEntitiesExcluding(Class<ExtraEntity> annotationClass) {
        Set<Class<?>> entityClasses = reflections.getTypesAnnotatedWith(Entity.class);
        Set<Class<?>> excludedClasses = reflections.getTypesAnnotatedWith(annotationClass);
        entityClasses.removeAll(excludedClasses);
        return entityClasses;
    }

    private Set<Class<?>> getEntitiesMatchingPrefix(String prefix) {
        return reflections.getTypesAnnotatedWith(ExtraEntity.class).stream()
                .filter(entityClass -> {
                    ExtraEntity extraEntityAnnotation = entityClass.getAnnotation(ExtraEntity.class);
                    return extraEntityAnnotation.prefix().equals(prefix);
                })
                .collect(Collectors.toSet());
    }

    private Set<String> extractPackageNames(Set<Class<?>> entityClasses) {
        return entityClasses.stream()
                .map(Class::getPackage)
                .map(Package::getName)
                .collect(Collectors.toSet());
    }
}

 

PrimaryPersistenceConfig


import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.*;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import test.dataSource.annotation.ExtraRepository;
import test.dataSource.component.EntitySearchHelper;
import test.dataSource.config.properties.PrimaryDataSourceProperties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Map;
import java.util.Set;

@Configuration
@EnableJpaRepositories(
        basePackages = {PrimaryPersistenceConfig.BASE_PACKAGE},
        entityManagerFactoryRef = PrimaryPersistenceConfig.PRIMARY_ENTITY_MANAGER_FACTORY,
        transactionManagerRef = PrimaryPersistenceConfig.PRIMARY_TRANSACTION_MANAGER,
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = ExtraRepository.class)
)
@RequiredArgsConstructor
public class PrimaryPersistenceConfig {
    public static final String PRIMARY_DATASOURCE_CONFIG_PREFIX = "primary";
    public static final String PRIMARY_PERSISTENCE_UNIT = PRIMARY_DATASOURCE_CONFIG_PREFIX + "PersistenceUnit";
    public static final String PRIMARY_DATASOURCE = PRIMARY_DATASOURCE_CONFIG_PREFIX + "DataSource";
    public static final String PRIMARY_ENTITY_MANAGER_FACTORY = PRIMARY_DATASOURCE_CONFIG_PREFIX + "EntityManagerFactory";
    public static final String PRIMARY_TRANSACTION_MANAGER = PRIMARY_DATASOURCE_CONFIG_PREFIX + "TransactionManager";
    public static final String BASE_PACKAGE = "test.dataSource";
    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;
    private final EntitySearchHelper entitySearchHelper;

    private final PrimaryDataSourceProperties properties;

    /**
     * Primary 데이터 소스 생성
     *
     * @return DataSource
     */
    @Primary
    @Bean(name = PRIMARY_DATASOURCE)
    public DataSource dataSource() {
        return properties.createDataSource();
    }

    /**
     * Primary 데이터 소스에 대한 EntityManagerFactory 생성
     *
     * @param builder    EntityManagerFactoryBuilder
     * @param dataSource DataSource
     * @return LocalContainerEntityManagerFactoryBean
     */
    @Primary
    @Bean(name = PRIMARY_ENTITY_MANAGER_FACTORY)
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier(PRIMARY_DATASOURCE) DataSource dataSource) {

        // 엔티티 클래스 패키지 조회
        Set<String> entityPackages = entitySearchHelper.findEntityPackagesOfPrimary();

        // 프로퍼티값 조회
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());

        return builder
                .dataSource(dataSource)
                .packages(entityPackages.toArray(new String[0]))
                .persistenceUnit(PRIMARY_PERSISTENCE_UNIT)
                .properties(properties)
                .build();
    }

    /**
     * Primary 데이터 소스에 대한 TransactionManager 생성
     *
     * @param entityManagerFactory EntityManagerFactory
     * @return PlatformTransactionManager
     */
    @Primary
    @Bean(name = PRIMARY_TRANSACTION_MANAGER)
    public PlatformTransactionManager transactionManager(
            @Qualifier(PRIMARY_ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

 

ExtraPersistenceConfig


import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import test.dataSource.annotation.ExtraRepository;
import test.dataSource.component.EntitySearchHelper;
import test.dataSource.config.properties.ExtraDataSourceProperties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Map;
import java.util.Set;

@Configuration
@EnableJpaRepositories(
        basePackages = {PrimaryPersistenceConfig.BASE_PACKAGE},
        entityManagerFactoryRef = ExtraPersistenceConfig.EXTRA_ENTITY_MANAGER_FACTORY,
        transactionManagerRef = ExtraPersistenceConfig.EXTRA_TRANSACTION_MANAGER,
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = ExtraRepository.class)
)
@RequiredArgsConstructor
public class ExtraPersistenceConfig {

    public static final String EXTRA_DATASOURCE_CONFIG_PREFIX = "extra";
    public static final String EXTRA_PERSISTENCE_UNIT = EXTRA_DATASOURCE_CONFIG_PREFIX + "PersistenceUnit";
    public static final String EXTRA_DATASOURCE = EXTRA_DATASOURCE_CONFIG_PREFIX + "DataSource";
    public static final String EXTRA_ENTITY_MANAGER_FACTORY = EXTRA_DATASOURCE_CONFIG_PREFIX + "EntityManagerFactory";
    public static final String EXTRA_TRANSACTION_MANAGER = EXTRA_DATASOURCE_CONFIG_PREFIX + "TransactionManager";

    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;
    private final EntitySearchHelper entitySearchHelper;

    private final ExtraDataSourceProperties properties;

    /**
     * Extra 데이터 소스 생성
     *
     * @return DataSource
     */
    @Bean(name = EXTRA_DATASOURCE)
    public DataSource extraDataSource() {
        return properties.createDataSource();
    }

    /**
     * Extra 데이터 소스의 EntityManagerFactory 생성
     *
     * @param builder    EntityManagerFactoryBuilder
     * @param dataSource DataSource
     * @return LocalContainerEntityManagerFactoryBean
     */
    @Bean(name = EXTRA_ENTITY_MANAGER_FACTORY)
    public LocalContainerEntityManagerFactoryBean extraEntityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier(EXTRA_DATASOURCE) DataSource dataSource) {

        // 엔티티 클래스 패키지 조회
        Set<String> entityPackages = entitySearchHelper.findEntityPackagesOfExtra(EXTRA_DATASOURCE_CONFIG_PREFIX);

        // 프로퍼티값 조회
        Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());

        return builder.dataSource(dataSource)
                .packages(entityPackages.toArray(new String[0]))
                .persistenceUnit(EXTRA_PERSISTENCE_UNIT)
                .properties(properties)
                .build();
    }

    /**
     * Extra 데이터 소스의 TransactionManager 생성
     *
     * @param entityManagerFactory EntityManagerFactory
     * @return PlatformTransactionManager
     */
    @Bean(name = EXTRA_TRANSACTION_MANAGER)
    public PlatformTransactionManager extraTransactionManager(
            @Qualifier(EXTRA_ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

 

ChainedTransactionConfig

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class ChainedTransactionConfig {

    public static final String CHAINED_TRANSACTION_MANAGER = "chainedTransactionManager";

    private final List<PlatformTransactionManager> transactionManagerList;


    @Bean(name = CHAINED_TRANSACTION_MANAGER)
    public ChainedTransactionManager chainedTransactionManager() {
        return new ChainedTransactionManager(transactionManagerList.toArray(new PlatformTransactionManager[0]));
    }
}

'Spring Jpa' 카테고리의 다른 글

JdbcTemplate을 활용하여 JPA의 saveAll() 대체하기  (0) 2023.09.23
JPA 관련 애노테이션 정리  (0) 2022.03.24
JPA DB 수동설정  (0) 2022.03.24
프록시와 연관관계 관리  (0) 2022.02.27
JPA Id 생성전략 설정하기  (0) 2022.02.27