안녕하세요^^ 이번 시간에는 Spring Data JPA에 master/slave DB Configuration 했던 저의 경험을 공유드리려 합니다.
더 좋은 아키텍쳐, 구조가 있다면 언제든지 댓글 부탁드립니다! 같이 성장해요!!
우선 가볍게 DB 소스에 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을 셋팅했습니다.
@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 정보들을 읽어 들입니다.
소스만 보셔도 딱! 이해가 되지 않나요??ㅎㅎ (별도 설명은 생략..)
@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된다고 하죠.
@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으로 잡아주었습니다.
@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와 같은 방식으로 설정해줍니다.
@Transactional(readOnly = true)
public interface AccountReadRepository extends JpaRepository<Account, Long> {
}
간단하죠??ㅎㅎ
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을 마쳤습니다.
더 좋은 방식있다면 꼭 댓글로 공유 부탁드립니다!!!
EntityManagerFactoryBuilder builder 이거 빈이 없다는데요;;;