
JPA์์ Muti Datasource ์ค์ ์ ๊ดํ ํธ๋ฌ๋ธ ์ํ ์ ๊ธฐ๋กํ ํฌ์คํ ์ ๋๋ค. ์ต์ข ์ธํ ๋ฐฉ์์ ํ๋จ์ ๊ธฐ์ฌ๋์ด ์์ต๋๋ค.
๋ฌธ์ ์ํฉ
์ ํฌ ์๋น์ค๋ ํ๋์ ํ๋ก์ ํธ์์ ์ฌ๋ฌ๋์ DataBase๋ฅผ ์ฐ๋ํ๋ ํ๊ฒฝ์ ๋๋ค. ๊ทธ ์ค์์ ๋ช๊ฐ์ DataBase๋ง ํ์์ ์ํด JPA/QueryDsl์ ์ฐ๋ํ์ฌ ์ฌ์ฉํ๋๋ฐ ์ต๊ทผ์ ์๋ก์ด Datasource๋ฅผ ์ถ๊ฐํ์ฌ QueryDsl์ ์ฌ์ฉํ๋ ์ค ๋ค์๊ณผ ๊ฐ์ connection leak warning์ด ๋จ๋ ๊ฒ์ ํ์ธํ์์ต๋๋ค.
java.lang.Exception: Apparent connection leak detected
๋ฌธ์ ์ ์์ธ์ ํ์ ํ๊ธฐ ์ํด hikari๋ก๊ทธ๋ฅผ ํ์ธํด๋ณด๋, ์ฟผ๋ฆฌ๊ฐ ๋ชจ๋ ์คํ๋ ํ์๋ active connenction์ด ์ ์ง๋๊ณ ์์๊ณ (์ปค๋ฅ์ ์ ๋ฐ๋ฉํ์ง ์์) ์ผ์ ์๊ฐ์ด ์ง๋๊ณ ๋๋ connection์ด ์์ ํ close๋์ด ์์ฒญ์ ์์ ์คํํ ์ ์๋ค๋ ์๋ฌ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
[HikariPool-3 housekeeper] [pool.HikariPool]- HikariPool-3 - Pool stats (total=10, active=1, idle=9, waiting=0)
..
could not prepare statement [Connection is closed] [select ...]
๋ฌธ์ ๊ฐ ์ฌํ๋ ์์ฒญ์ ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ๊ฐ ์๋ ์์ฒญ์ด์๊ณ QueryDsl์ ์ฌ์ฉํ ๊ฒฝ์ฐ์๋ง ๋ฐ์ํ๋ค๋ ์ ์ ๋ณด์ ์ค์ ์ ๋ฌธ์ ๊ฐ ์์ ๊ฒ์ด๋ผ ์ถ์ธกํ ์ ์์์ต๋๋ค.
์๋ชป๋ ์ฝ๋
์๋๋ ๋ฌธ์ ๊ฐ ๋์๋ ์ฝ๋์ ์์์ ๋๋ค.
@Bean("firstJpaQueryfactory")
public JPAQueryFactory firstJpaQueryfactory(
@Qualifier("firstEntityManagerFactory") EntityManagerFatory entityManagerFatory
) {
EntityManager entityManager = entityManagerFatory.createEntityManager();
return new JPAQueryFactory(entityManager);
}
@Bean("secondJpaQueryfactory")
public JPAQueryFactory secondJpaQueryfactory(
@Qualifier("secondEntityManagerFactory") EntityManagerFatory entityManagerFatory
) {
EntityManager entityManager = entityManagerFatory.createEntityManager();
return new JPAQueryFactory(entityManager);
}
ํด๋น ์ฝ๋๋ ๋ฉํฐ Datasource๋ฅผ ์ฌ์ฉํ๋ JPAQueryFactory๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๋ ์ฝ๋์ ๋๋ค.
๋จ์ผ DB ํ๊ฒฝ์์๋ Spring์ด ์๋์ผ๋ก ๋น์ ์ฃผ์
ํด์ฃผ๊ธฐ๋๋ฌธ์(Querydsl 4.์ด์) ๋ณ๋ ์ค์ ์์ด๋ JPAQueryFactory๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ฉํฐ DB ํ๊ฒฝ์์๋ ๋์ผํ ํ์
์ DataSource bean์ด ์ฌ๋ฌ ๊ฐ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์ด๋ค DataSource๋ฅผ ์ฌ์ฉํด์ผ ํ ์ง ํ๋จํ ์ ์๋๋ก ์ฌ์ฉํ๊ณ ์ํ๋ DataSource ๋ณ EntityManagerFactory์ TransactionManager, JPAQueryFactory์ ๊ฐ์ JPA ๊ด๋ จ bean๋ค์ ๋ช
์์ ์ผ๋ก ๊ตฌ์ฑํด์ฃผ์ด์ผ ํฉ๋๋ค.
๋ฌธ์ ์์ธ
(๋ฌธ์ ๊ฐ ๋ฌด์์ธ์ง ์ฐพ์ผ์
จ๋์?)
์ ์ฝ๋๊ฐ ๋ฌธ์ ๊ฐ ๋์๋ ์ด์ ๋ JPAQueryFactory์ EntityManager๋ฅผ ์ง์ ์ฃผ์
ํ๊ธฐ ๋๋ฌธ์
๋๋ค.
JPA๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ฒญ ๋น ํ๋์ EntityManager๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๊ฐ EntityManager๋ ๋ด๋ถ์ ์ผ๋ก DB ์ปค๋ฅ์ ํ์ ํตํด DB์ ์ ๊ทผํฉ๋๋ค. EntityManager๋ thread-safeํ์ง ์๊ธฐ ๋๋ฌธ์ ์ค๋ ๋ ๊ฐ ๊ณต์ ๋์ด์๋ ์ ๋๋ฉฐ, ํธ๋์ญ์ ์ด ์๋ฃ๋ ํ์๋ ์ปค๋ฅ์ ์ ๋ฐ๋ฉํด์ผ ํฉ๋๋ค.
์คํ๋ง๊ณผ ๊ฐ์ ์ปจํ
์ด๋ ํ๊ฒฝ์์๋ ๊ฐ๋ฐ์๊ฐ EntityManager๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ์ปจํ
์ด๋์ ์์ํ๋๋ฐ, ์คํ๋ง์ ์ฑ๊ธํค ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋น์ ์์ฑ๊ฐ์ ๋ชจ๋ ์ค๋ ๋๊ฐ ๊ณต์ ํ๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ ์คํ๋ง์ EntityManager์ thread-safe๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํด ํ๋ก์๋ฅผ ํตํด EntityManager๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
ํ๋ก์ ๊ฐ์ฒด๋ ํธ๋์ญ์
๋ฒ์์ ๋ฐ๋ผ EntityManager๋ฅผ ์์ฑํ๊ณ ๊ด๋ฆฌํฉ๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ ํธ๋์ญ์
๋ง๋ค ์๋ก์ด EntityManager๊ฐ ์์ฑ๋๋ฉฐ, ํธ๋์ญ์
์ด ์ข
๋ฃ๋๋ฉด ์๋์ผ๋ก EntityManager๊ฐ ๋ซํ๊ณ ์ปค๋ฅ์
์ด ๋ฐํ๋ฉ๋๋ค.
์ ์ฝ๋์ ๊ฐ์ด EntityManagerFatory๋ฅผ ์ฃผ์ ๋ฐ์ EntityManager๋ฅผ ์์ฑํ ๊ฒฝ์ฐ JPAQueryFactory๊ฐ ํ๋ก์๊ฐ ์๋ ์ค์ EntityManager ์ธ์คํด์ค๋ฅผ ์ง์ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ์ด๋ก ์ธํด ์์ฒญ์ด ์ข ๋ฃ๋์ด๋ ์ปค๋ฅ์ ์ด ๋ฐ๋ฉ๋์ง ์๊ณ , ๋ชจ๋ ์์ฒญ์์ ๋์ผํ ์ปค๋ฅ์ ์ ์ฌ์ฌ์ฉํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ ๊ฒ ์ ๋๋ค.
EntityManager๊ฐ thread-safeํ์ง ์์ผ๋ฉด race condition์ด ๋ฐ์ํ ์ ์์ผ๋ฉฐ, ๋ค๋ฅธ ์ค๋ ๋์์ ์๊ธฐ์น ์๊ฒ flush๋ฅผ ์ํํ๊ฑฐ๋ ์ํฐํฐ๋ฅผ ์กฐํํ๋ ๋ฑ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
ํด๊ฒฐ๋ฐฉ๋ฒ ๋ฐ ๋ณ๊ฒฝ์ฌํญ
EntityManager๋ thread-safeํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด์๋ EntityManager ๊ฐ์ฒด๋ฅผ bean์ผ๋ก ๋ฑ๋กํ๋ฉด ์๋๊ณ ์ง์ ํธ์ถํ ๊ฒฝ์ฐ์๋ @PersistenceContext๋ฅผ ํตํด ์ฃผ์
๋ฐ์ ์ฌ์ฉํด์ผํฉ๋๋ค.
์๋ชป๋ ์ฃผ์ ๋ฐฉ๋ฒ
@Service
public class ProblematicService {
private final EntityManagerFactory emf;
private final EntityManager em; // ์ง์ ์์ฑํ EntityManager
public ProblematicService(EntityManagerFactory emf) {
this.emf = emf;
this.em = emf.createEntityManager(); // ์ด๋ ๊ฒ ํ๋ฉด ์ ๋จ!
}
}
์ฌ๋ฐ๋ฅธ ์ฃผ์ ๋ฐฉ๋ฒ
@Service
public class RecommendedService {
// 1. ์์ฑ์ ์ฃผ์
private final EntityManager em;
public RecommendedService(EntityManager em) {
this.em = em;
}
}
@Service
public class RecommendedService {
// 2. @PersistenceContext ์ฌ์ฉ
@PersistenceContext
private EntityManager entityManager;
}
Spring boot 2.0 ๋ถํฐ๋ @Autowired๋ก๋ EntityManager์ฃผ์ ์ด ๊ฐ๋ฅํฉ๋๋ค. ์คํ๋ง๋ถํธ๊ฐ ์๋์ผ๋ก @PersistenceContext๋ก ๋์ฒดํด ๋น์ ๋ฑ๋กํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ ๋ฌธ์ ์ ์ฝ๋๋ฅผ ์๋์ ๊ฐ์ด ๊ฐ์ ํ์์ต๋๋ค.
@Configuration
public class QuerydslConfig {
@PersistenceContext(unitName = "firstEntityManager")
private EntityManager firstEntityManager;
@PersistenceContext(unitName = "secondEntityManager")
private EntityManager secondEntityManager;
@PersistenceContext(unitName = "thirdEntityManager")
private EntityManager thirdEntityManager;
@Primary
@Bean("firstjpaQueryFactory")
public JPAQueryFactory firstJpaQueryFactory() {
return new JPAQueryFactory(firstEntityManager);
}
@Bean("secondJpaQueryFactory")
public JPAQueryFactory secondJpaQueryFactory() {
return new JPAQueryFactory(secondEntityManager);
}
@Bean("thirdJpaQueryfactory")
public JPAQueryFactory thirdJpaQueryfactory() {
return new JPAQueryFactory(thirdEntityManager);
}
}
Muti Datasource ์ค์ ๊ฐ์ด๋
์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ต์ข ์ค์ ์ ๋ค์๊ณผ ๊ฐ์ด ์ ๋ฆฌํ์์ต๋๋ค.
Datasource bean ์ถ๊ฐ
@Configuration
public class DataSourceConfig {
private static final String FIRST_DATASOURCE = "firstDataSource";
private static final String SECOND_DATASOURCE = "secondDataSource";
private static final String THIRD_DATASOURCE = "thirdDataSource";
@Primary // ๋ฉ์ธ DB
@Bean(FIRST_DATASOURCE)
@ConfigurationProperties(prfix = "spring.datasource.first")
public DataSource corownDataSource(){
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(SECOND_DATASOURCE)
@ConfigurationProperties(prfix = "spring.datasource.second")
public DataSource corbatDataSource(){
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(THIRD_DATASOURCE)
@ConfigurationProperties(prfix = "spring.datasource.third")
public DataSource custonDataSource(){
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
Transaction config ์ถ๊ฐ
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "kr.co.sample.domain.first", // ํด๋น DB๊ฐ ์๋ ํจํค์ง(ํด๋) ๊ฒฝ๋ก
entityManagerFactoryRef = "firstEntityManagerFactory", // EntityManagerFactory ์ด๋ฆ
transactionManagerRef = "firstTransactionManager" // TransactionManager ์ด๋ฆ
)
public class FirstTransactionConfig {
@Autowired
@Qualifier("firstDataSource")
private DataSource firstDataSource;
@Bean(name = "firstEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(customDataSource)
.packages("kr.co.sample.domain.first.entity") // ์ฒซ๋ฒ์งธ DB์ ๊ด๋ จ๋ ์ํฐํฐ๋ค์ด ์๋ ํจํค์ง(ํด๋) ๊ฒฝ๋ก
.persistenceUnit("firstEntityManager") // EntityManager unit ์ด๋ฆ
.build();
}
@Bean(name = "firstTransactionManager")
public PlatformTransactionManager firstTransactionManager(
final @Qualifier("firstEntityManagerFactory") LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean
) {
return new JpaTransactionManager(localContainerEntityManagerFactoryBean.getObject());
}
}
JpaQueryFactory bean ์ถ๊ฐ (QueryDsl ์ฌ์ฉ์)
@Configuration
public class QuerydslConfig {
@PersistenceContext(unitName = "firstEntityManager")
private EntityManager corownEntityManager;
@PersistenceContext(unitName = "secondEntityManager")
private EntityManager secondEntityManager;
@PersistenceContext(unitName = "thirdEntityManager") // EntityManager unit ์ด๋ฆ
private EntityManager thirdEntityManager;
@Primary
@Bean("firstjpaQueryFactory")
public JPAQueryFactory firstJpaQueryFactory() {
return new JPAQueryFactory(firstEntityManager);
}
@Bean("secondJpaQueryFactory")
public JPAQueryFactory secondJpaQueryFactory() {
return new JPAQueryFactory(corbatEntityManager);
}
@Bean("thirdJpaQueryFactory")
public JPAQueryFactory thirdJpaQueryFactory() {
return new JPAQueryFactory(customEntityManager);
}
}
์ปค์คํ Tranactional ์ด๋ ธํ ์ด์ ์ถ๊ฐ
@Transaction ์ด๋
ธํ
์ด์
๋ํ default๋ก Primary TransactionManager ๋น์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์
์ถ๊ฐ DataSource์ ๋ํ ํธ๋์ญ์
์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ value ์ต์
์ ์ง์ ํด์ฃผ์ด์ผํฉ๋๋ค.
@Transactional(value = "secondTransactionManager")
์์ฃผ ์ฌ์ฉ๋๋ ํธ๋์ญ์ ์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด custom transaction annotation์ ์์ฑํด์ ์ฌ์ฉํ๋๋ก ํฉ๋๋ค.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional
public @interface SecondTransaction {
String transactionManager() default "secondTransactionManager"; // TransactionManager ์ด๋ฆ
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
}
์ปค์คํ
์ด๋
ธํ
์ด์
์ @Transactional๋ง ๋ถ์ฌ์ฃผ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก @Transactional๊ณผ ๊ฐ์ ๋์์ ํฉ๋๋ค.
readOnly์ ๊ฐ์ @Transactional์ ์์ฑ๋ค๊ณผ ๋งค์นญ๋๋ ์์ฑ์ ์ ์ํ๋ฉด ์คํ๋ง์ด ์ด๋ฅผ ์ฝ๊ณ @Transactional์ ์์ฑ์ผ๋ก ์ธ์ํ์ฌ ๋์ํฉ๋๋ค.
์ด๋ฒ ํฌ์คํ ์ Spring์ EntityManager ํ๋ก์ ๊ธฐ๋ฅ๊ณผ Thread-safe์์ ๊ด๊ณ๋ฅผ ๊ฐ๊ณผํ๊ณ ๋ฐ์ํ๋ ์ด์๋ฅผ ๋ค๋ค๋ดค์ต๋๋ค. Spring์์ ์ ๊ณตํด์ฃผ๋ ๊ธฐ๋ฅ์ ์ปค์คํ ํ๊ฒ ์ฌ์ฉํ ๊ฒฝ์ฐ์๋ ํญ์ ์ฃผ์๊น๊ฒ ์ดํด์ผํ๋ค๋ ๊ฑธ ๋ค์ ํ๋ฒ ๋๊ผ์ต๋๋ค. ๋๊น์ง ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค ๐ค
'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Spring Security์์ ๋ฐ์ํ ์์ธ๊ฐ ControllerAdvice์์ ํธ๋ค๋ง๋๋ ๋ฌธ์ (2) | 2024.04.25 |
|---|---|
| [Spring] OpenFeign์ ์ด์ฉํ์ฌ API ํต์ ํด๋ณด๊ธฐ (feat. ๊ณ ์์ด ์ฌ์ง ๊ฒ์) (2) | 2023.04.25 |
| [Spring] Filter์ Interceptor ์ฐจ์ด (2) | 2023.04.22 |
| [JPA] ์ปค์ ๊ธฐ๋ฐ pagenation ๊ตฌํํ๊ธฐ (8) | 2023.03.01 |
| [JPA] ์คํ์ ๊ธฐ๋ฐ Pagenation ๊ตฌํํ๊ธฐ (0) | 2023.03.01 |
๋๊ธ