Spring Data JPA master/slave DB Config (패키지 분리 방식)

MOOZZANG·2021년 5월 1일
1

스프링 부트

목록 보기
1/1
post-thumbnail
post-custom-banner

안녕하세요^^ 이번 시간에는 Spring Data JPA에 master/slave DB Configuration 했던 저의 경험을 공유드리려 합니다.

더 좋은 아키텍쳐, 구조가 있다면 언제든지 댓글 부탁드립니다! 같이 성장해요!!

이번 시간에는 read/write 패키지 분기 처리 방식으로 구현해보겠습니다.

우선 가볍게 DB 소스에 yml 셋팅 하겠습니다.

application.yml

spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        format_sql: true


db:
  datasource:
    writeurl: jdbc:mysql://smlee-db.cluster-ce0nnd4zss97.ap-northeast-2.rds.amazonaws.com:3306/moomoodb?useSSL=false&useUnicode=true&characterEncoding=utf8
    readurl: jdbc:mysql://smlee-db.cluster-ro-ce0nnd4zss97.ap-northeast-2.rds.amazonaws.com:3306/moomoodb?useSSL=false&useUnicode=true&characterEncoding=utf8
    username: moomoo
    password: 12345678
    driver: com.mysql.cj.jdbc.Driver

spring jpa 쪽에는 기본적인 jpa 로깅 셋팅, 그리고 hibernate 셋팅을 진행합니다.
참고로 ddl-auto 셋팅은 편하신대로 셋팅하시면 됩니다!

저는 별도의 prefix(db.datasource)를 만들어서 writeurl, readurl을 셋팅했습니다.

프로젝트 구조

db.datasouce를 읽어들이는 Properties 객체 만들기



@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "db.datasource")
public class DBProperty {
    @NotEmpty
    private String writeurl;
    @NotEmpty
    private String readurl;
    @NotEmpty
    private String username;
    @NotEmpty
    private String password;
    @NotEmpty
    private String driver;
}

DBProperty.class는 application.yml에서 정의한 db.datasource 정보들을 읽어 들입니다.
소스만 보셔도 딱! 이해가 되지 않나요??ㅎㅎ (별도 설명은 생략..)

이제는 DataSource를 Config 해봅시다!


@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {

    private final DBProperty dbProperty;

    @Bean(name = "writeDataSource")
    public HikariDataSource writeDataSource() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(dbProperty.getDriver());
        hikariDataSource.setJdbcUrl(dbProperty.getWriteurl());
        hikariDataSource.setUsername(dbProperty.getUsername());
        hikariDataSource.setPassword(dbProperty.getPassword());
        return hikariDataSource;
    }

    @Bean(name = "readDataSource")
    public HikariDataSource readDataSource() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(dbProperty.getDriver());
        hikariDataSource.setJdbcUrl(dbProperty.getReadurl());
        hikariDataSource.setUsername(dbProperty.getUsername());
        hikariDataSource.setPassword(dbProperty.getPassword());
        return hikariDataSource;
    }

}

위에서 셋팅한 DBProperty를 기반으로 writeDataSource와 readDataSource를 생성해줍니다!
여기서는 HikariDataSource 타입 인스턴스로 DataSource를 생성하였는데요.

Hikari는 connection pooling 메커니즘을 제공하는 Jdbc DataSource 구현체 중 하나입니다.
다른 구현체들에 비해, lightweight하며 뛰어난 performance를 자랑합니다.
특히 Spring Boot에서도 DataSource는 HikariDataSource로 Auto-Config된다고 하죠.

Master JPA Configuration 설정


@Slf4j
@Configuration
@EnableJpaRepositories(
        basePackages = "me.seungmoo.subject.repository.write",
        entityManagerFactoryRef = "writeEntityManagerFactory",
        transactionManagerRef = "writeTransactionManager"
)
public class WriteJpaConfig {

    private final HikariDataSource writeDataSource;
    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;

    public WriteJpaConfig(@Qualifier("writeDataSource") HikariDataSource writeDataSource, JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
        log.info("writeDataSource.getJdbcUrl() : " + writeDataSource.getJdbcUrl());
        this.writeDataSource = writeDataSource;
        this.jpaProperties = jpaProperties;
        this.hibernateProperties = hibernateProperties;
    }

    @Bean("writeEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean writeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        Map<String, Object> props = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());

        return builder.dataSource(writeDataSource)
                .packages("me.seungmoo.subject")
                .persistenceUnit("write")
                .properties(props)
                .build();
    }

    @Bean("writeTransactionManager")
    @Primary
    public PlatformTransactionManager writeTransactionManager(
            @Qualifier("writeEntityManagerFactory") EntityManagerFactory writeEntityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(writeEntityManagerFactory);
        return tm;
    }
}

WriteJpaConfig 라는 Configuration Bean 객체를 만들어 줍니다!
application.yml에서 설정한 jpa, hibernate 설정 정보는 JpaProperties, HibernateProperties Bean을 통해 주입 받습니다.

그리고

자 이제 writeEntityManagerFactory를 통해 Master DB에 대한 Entity Manager를 셋팅해줍니다! 여기서 가장 중요한 부분은 .persistenceUnit("write") 입니다.

persistenceUnit을 통해 영속성 관리 단위를 설정해줄 수 있는데요.
@EnableJpaRepositories에서 me.seungmoo.subject.repository.write 를 basePackage로 잡아줌 으로써, write 패키지에 있는 JpaRepository를 write persistenceUnit으로 잡아주었습니다.

Slave JPA Config


@Slf4j
@Configuration
@EnableJpaRepositories(
        basePackages = {"me.seungmoo.subject.repository.read"},
        entityManagerFactoryRef = "readEntityManagerFactory",
        transactionManagerRef = "readTransactionManager"
)
public class ReadJpaConfig {

    private final HikariDataSource readDataSource;
    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;

    public ReadJpaConfig(@Qualifier("readDataSource") HikariDataSource readDataSource, JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
        log.info("readDataSource.getJdbcUrl() : " + readDataSource.getJdbcUrl());
        this.readDataSource = readDataSource;
        this.jpaProperties = jpaProperties;
        this.hibernateProperties = hibernateProperties;
    }

    @Bean("readEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean readEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        Map<String, Object> props = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());

        return builder.dataSource(readDataSource)
                .packages("me.seungmoo.subject")
                .persistenceUnit("read")
                .properties(props)
                .build();
    }

    @Bean("readTransactionManager")
    public PlatformTransactionManager readTransactionManager(
            @Qualifier("readEntityManagerFactory") EntityManagerFactory readEntityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(readEntityManagerFactory);
        return tm;
    }
}

ReadJpaConfig도 Write와 같은 방식으로 설정해줍니다.

위에서 Config한 내용을 바탕으로 Repository를 생성해주면 됩니다.

AccountReadRepository만 간단하게 살펴보겠습니다.


@Transactional(readOnly = true)
public interface AccountReadRepository extends JpaRepository<Account, Long> {
}

간단하죠??ㅎㅎ

QueryDsl을 적용한 JpaRepository 확장 클래스 구현하기

인터페이스


public interface ProductReadRepositoryExtention {
    Page<ProductInfoDto> findAllProduct(Pageable pageable);
    Page<InvestsOfAccountDTO> findInvestsOfAccount(Account account, Pageable pageable);
}

구현 클래스


public class ProductReadRepositoryExtentionImpl extends QuerydslRepositorySupport implements ProductReadRepositoryExtention {

    public ProductReadRepositoryExtentionImpl() {
        super(Product.class);
    }

    @Override
    @PersistenceContext(unitName = "read")
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }

    /**
     * Find All Product(invest) List
     * @return
     */
    @Override
    public Page<ProductInfoDto> findAllProduct(Pageable pageable) {
        QProduct product = QProduct.product;
        QInvestTransaction investTransaction = QInvestTransaction.investTransaction;

        LocalDateTime now = LocalDateTime.now();

        JPQLQuery<ProductInfoDto> query = from(product)
                .where(product.finishedAt.after(now).and(product.startedAt.before(now)))
                .select(Projections.constructor(ProductInfoDto.class,
                        product.productId,
                        product.title,
                        product.totalInvestingAmount,
                        product.investStat,
                        product.startedAt,
                        product.finishedAt,
                        ExpressionUtils.as(
                                JPAExpressions.select(investTransaction.money.sum())
                                        .from(investTransaction)
                                        .where(investTransaction.product.productId.eq(product.productId)),
                                "nowAmount")
                        ));

        JPQLQuery<ProductInfoDto> productJPQLQuery = getQuerydsl().applyPagination(pageable, query);

        QueryResults<ProductInfoDto> productQueryResults = productJPQLQuery.fetchResults();
        return new PageImpl<>(productQueryResults.getResults(), pageable, productQueryResults.getTotal());
    }

}

Spring Data JPA를 사용하신다면 역시... QueryDsl 빼놓을 수 없죠...
QuerydslRepositorySupport 을 상속받는 Repository 확장 클래스를 만들 경우, 필수적으로 셋팅해주실 부분이 있습니다!!

바로 이부분!!!!!


    @Override
    @PersistenceContext(unitName = "read")
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }
    

위의 JpaConfig에서 persistenceUnit 잡아주셨죠?? read 명시한 부분으로 Repository 확장 클래스에서도 잡아주시면 되겠습니다!

총평

이렇게 패키지 분리 방식으로 구현하게 되는 경우, Transactional 분기 방식에 비해 약간의 자유도?가 존재하게 됩니다.
DB Replication의 경우 MasterDB와 SlaveDB 간의 Lag는 반드시 발생하게 되는데요.
Lag로 인해 어쩔 수 없이 Master DB에서도 read 작업이 있어야 한다 등의 작업에 대해서, 개인적으로 패키지 분리 방식도 괜찮지 않나 싶습니다 ㅎㅎ;;

추가로 셋팅이 좀 더 편리하다는 장점도 있습니다!!

이렇게 하여 기본적인 Spring Data JPA의 Multi DataSource Configuration을 마쳤습니다.
더 좋은 방식있다면 꼭 댓글로 공유 부탁드립니다!!!

profile
느리지만 꾸준히 성장하는 개발자
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 8월 16일

EntityManagerFactoryBuilder builder 이거 빈이 없다는데요;;;

답글 달기
comment-user-thumbnail
2024년 9월 23일

다 라우팅 방식이라 기존방식과 다르고 운영중 이슈가 잇을 가능성이 높아 서칭중이었는데 적절한걸 찾았네요 아직은 이런 명시적으로 패키지를 나누거나 파일을 나누는 방식이 편한것 같고 이슈가 없을 것 같아요 좋은 글 감사합니다

답글 달기