1. Background
μ΄μ μ°Έμ¬νλ νλ‘μ νΈ μ€μ transform() μ μ¬μ©νλ λ‘μ§μμ λ°μνλ λ΄μ© κΈ°λ‘μ© κΈμ΄λ€.
- μμ μ½λμ νλ‘μ νΈ νκ²½μ Spring Boot 2.7.15, QueryDSL 5.0 μ΄λ€.
(1) μν μ½λλ₯Ό νμΈν΄λ³΄μ.
λ¬Έμ λ querydsl μμ μ 곡νλ transform() λ©μλλ₯Ό μ¬μ©νλ κ³Όμ μμ connection leak μ΄ λ°μνλ€. connection pool μ DBμ 쿼리 μμ²μ΄ μλ£λ connection μ λ€μ νμνμ§λ§ λ°νλμ§ μλ λ¬Έμ κ° λ°μνλ©° JDBC ConnectionException μ λ°ννμλ€. μλλ 쿼리 μμμ΄λ€. (μ€μ μ½λ λμ μ μ¬ν μ½λλ₯Ό μμ±ν μνμ΄λ€.)
@Repository
@RequiredArgsConstructor
public class StudentRepositoryImpl implements StudentRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public List<StudentLookupResponse> findAllByStudentIds(List<Long> studentIds) {
return jpaQueryFactory.select(student)
.from(student)
.leftJoin(award).on(student.id.eq(award.student.id))
.where(student.id.in(studentIds))
.transform(groupBy(student.id)
.list(Projections.constructor(StudentLookupResponse.class,
student.id,
student.name,
student.tagName,
GroupBy.list(
Projections.constructor(AwardLookupResponse.class,
award.id,
award.name))
)
)
);
}
}
(2) debug μ€μ μ ν΅ν΄ νμΈν΄λ³΄μ.
μΆκ°μ μΌλ‘ Hikari Pool μ active, idle μνλ₯Ό λλ²κΉ μ€μ μ ν΅ν΄ νμΈνμλ€.

μ°μ κ²°λ‘ λΆν° μ΄μΌκΈ°νμλ©΄ transform() λ©μλμμ @Transactional μ΄ μ μΈλμ΄ μμ§ μμΌλ©΄ connection leak μ΄ λ°μνλ€.
κ·Έ μ΄μ λ μλ QueryDSL μμ€ μ½λλ₯Ό ν΅ν΄ νμΈν΄λ³΄μ.
2. QueryDSL λ΄λΆ μ½λλ₯Ό νμΈν΄λ³΄μ.
μ‘°κΈ λ μΈλΆμ μΈ κ²°λ‘ μ transform() λ©μλμμ @Transactional μ μ μΈνμ§ μμΌλ©΄ EntityManager λ₯Ό λ«νμ§ μμ λ°μνλ λ¬Έμ λ€. μ΄λ₯Ό νμΈν΄λ³΄κΈ° μν΄ transform() λ©μλ λ΄λΆλ₯Ό νμΈν΄λ³΄μ.
transform() λ©μλλ λ΄λΆμ μΌλ‘ iterate() λ©μλλ₯Ό νΈμΆνλλ° queryHandler.iterate() λ₯Ό νΈμΆνλ€.
κ·Έλ¦¬κ³ λ΄λΆμ μΌλ‘ SharedEntityManagerCreator.invoke() λ©μλλ₯Ό νΈμΆνλλ° λ‘μ§μ μμ½νμλ©΄ μλμ κ°λ€.
(1) SharedEntityManagerCreator.invoke() μμ EntityManager λ₯Ό λ«νμΌ νλ€.
- νμ¬ νΈλμμ μ μ°Έμ¬νκ³ μλ EntityManager λ₯Ό μ‘°ννλ€. (μ‘΄μ¬νμ§ μμ κ²½μ° μλ‘μ΄ EntityManager λ₯Ό μμ±νλ€.)
- EntityManager μ λ©μλλ₯Ό νμ©νκΈ° μν΄μ DeferredQueryInvocationHandler Proxy κ°μ²΄λ₯Ό μμ±ν΄μ λ°ννλ€.
- DeferedQueryInvocationHandler μ SharedEntityManagerμμ λΉνΈλμμ createQuery()κ° νΈμΆλ λ ν΄λΉ 쿼리 κ°μ²΄λ₯Ό μ²λ¦¬νλ€.
- (π¨ν΅μ¬!!) DeferedQueryInvocationHandler proxyμ invoke() λ©μλλ₯Ό μ€ννκ³ queryTerminationMethod 미리 μ μν΄λ method nameμΌ κ²½μ°μλ entityManagerλ₯Ό μ’ λ£νλ€.
public abstract class SharedEntityManagerCreator {
private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// (1) νΈλμμ
μ°Έμ¬νλ©΄ target κ°μ λ°ννμ§ μλλ€.
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
this.targetFactory, this.properties, this.synchronizedWithTransaction);
// ...
try {
Object result = method.invoke(target, args);
if (result instanceof Query) {
Query query = (Query)result;
if (isNewEm) {
Class<?>[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key ->
ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader));
// (2) DeferredQueryInvocationHandler μμ±ν΄μ λ€λ₯Έ κ³³μμ νΈμΆ
result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs,
new DeferredQueryInvocationHandler(query, target));
isNewEm = false;
} else {
EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory);
}
}
return result;
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
} finally {
if (isNewEm) {
EntityManagerFactoryUtils.closeEntityManager(target);
}
}
}
private static class DeferredQueryInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
try {
// ...
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
} finally {
// (3) queryTerminatingMethods ν¬ν¨λλ©΄ EntityManager λ₯Ό λ«λλ€.
if (queryTerminatingMethods.contains(method.getName())) {
if (this.outputParameters != null && this.target instanceof StoredProcedureQuery) {
StoredProcedureQuery storedProc = (StoredProcedureQuery)this.target;
for (Map.Entry<Object, Object> entry : this.outputParameters.entrySet()) {
try {
Object key = entry.getKey();
if (key instanceof Integer) {
entry.setValue(storedProc.getOutputParameterValue((Integer)key));
} else {
entry.setValue(storedProc.getOutputParameterValue(key.toString()));
}
} catch (IllegalArgumentException ex) {
entry.setValue(ex);
}
}
}
EntityManagerFactoryUtils.closeEntityManager(this.entityManager);
this.entityManager = null;
}
}
}
}
}
}
(2) queryTerminationMethods μ λ©μλκ° ν¬ν¨λμ΄ μμ΄μΌ EntityManager κ° λ«νλ€.
μ μ½λμ 3λ² μ£Όμμ νμΈν΄λ³΄λ©΄ queryTerminatingMethods μ μ μλ λ©μλ μ΄λ¦μ ν΅ν΄ μν°ν° λ§€λμ λ₯Ό λ«λλ€. νλ² queryTerminatingMethods λ₯Ό νμΈν΄λ³΄μ.

SharedEntityManagerCreator μ static block μ νμΈν΄λ³΄λ©΄ iterate() λ©μλκ° ν¬ν¨λμ΄ μμ§ μλ€. μ¦, κΈ°λ³Έμ μΌλ‘λ transform() λ©μλμ λ΄λΆ νΈμΆ λ©μλμΈ iterate() νΈμΆλλ©΄ EntityManager λ₯Ό λ«λ λ©μλμ ν΄λΉλμ§ μκΈ° λλ¬Έμ EntityManager λ₯Ό λ«νμ§ μμ connection leak μ μΌκΈ°ν μ μλ€λ κ²μ΄λ€.
νμ§λ§ μ¬κΈ°μ μλ¬Έμ μ΄ λ¨μμλ€. κ·Έλ λ€λ©΄ μ @Transactional μ μ μΈνλ©΄ EntityManager κ° λ«νλ κ²μΌκΉ?

3. @Transactional μ ν΅ν΄ μ΄λ»κ² EntityManager λ₯Ό λ«μκΉ?
(1) @Transactional μ μμ μ΄ μλ£λλ©΄ κΈ°λ³Έμ μΌλ‘ EntityManager λ₯Ό λ«λλ€.

μ΄μ μ μλ¬Έμ μ ν΄κ²°ν΄λ³΄μ. κ·Έ μ΄μ μ μ€νλ§μμ μ 곡νλ @Transactional μ μ μΈ μμ λμ λ°©μμ 보μ. νΈλμμ μ μννλλ° μ¬λ¬ μ»΄ν¬λνΈλ€μ΄ μ¬μ©λμ§λ§ ν΅μ¬ μ»΄ν¬λνΈλ TransactionInterceptor μ΄λ€. TransactionInterceptor μ λΆλͺ¨ ν΄λμ€μΈ AbstractPlatformTransactionManager μ invokeWithinTransaction() λ©μλλ₯Ό νΈμΆνλ©° μ 체 νΈλμμ μ΄ λμνλ€.
(2) ν΅μ¬μAbstractPlatformTransactionManager. invokeWithinTransaction() μ μλ€.
AbstractPlatformTransactionManager μ invokeWithinTransaction() λ΄λΆ νΈμΆ λ©μλμΈ cleanupAfterCompletion() λ©μλλ₯Ό νμΈν΄λ³΄μ. ν΄λΉ λ©μλλ νΈλμμ μ΄ μλ£(commit or rollback) μμ μ ThreadLocal μ νΈλμμ μ 보λ₯Ό λ°ννλ©΄μ μν°ν° λ§€λμ λ₯Ό λ«λλ€. EntityManager λ₯Ό λ«μΌλ©΄μ μ¬μ©ν Connection μ ConnectionPool μ λ°ννκΈ° λλ¬Έμ @Transactional μ΄λ Έν μ΄μ μ μ μΈνλ©΄ Connection μ λ°ννλ€.
π¨ κ²°λ‘ : transform() λ©μλμ connection leak μ λ°©μ§νκΈ° μν΄μλ κΌ @Transactional μ μ μΈν΄μ μ¬μ©νλλ‘ ν΄μΌ νλ€.
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
try {
// ...
else if (status.isNewTransaction()) {
// ...
doCommit(status); // 컀λ°νκΈ° (ꡬν체: JpaTransactionManager)
}
// ...
}
} finally {
// νΈλμμ
μ 보 μ κ±°
cleanupAfterCompletion(status);
}
}
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
status.setCompleted();
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
doCleanupAfterCompletion(status.getTransaction()); // μ¬κΈ° λ€μ΄κ°λ©΄ EntityManager λ€λ λ‘μ§ μμ.
}
if (status.getSuspendedResources() != null) {
if (status.isDebug()) {
logger.debug("Resuming suspended transaction after completion of inner transaction");
}
Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
resume(transaction, (SuspendedResourcesHolder)status.getSuspendedResources());
}
}
@Override
protected void doCleanupAfterCompletion(Object transaction) {
// ...
if (txObject.isNewEntityManagerHolder()) {
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
if (logger.isDebugEnabled()) {
logger.debug("Closing JPA EntityManager [" + em + "] after transaction");
}
EntityManagerFactoryUtils.closeEntityManager(em); // EntityManager λ«κΈ°!
} else {
logger.debug("Not closing pre-bound JPA EntityManager after transaction");
}
}
}
4. (λ²μΈ) QueryDSL? Hibernate? μμ ν΄κ²°ν λ¬Έμ μΈκ°?

μ€μ QueryDSL repository μμλ κ΄λ ¨ μ΄μμ λν΄ μ΄μΌκΈ°νκ³ μμ§λ§ querydsl κ° μλ κ²μΌλ‘ νλ¨νκ³ μλ€. κ·Έ μ΄μ λ νΈλμμ μ΄ νμ±νλμ§ μμ λλ Connection μ μμ±νλ 주체λ Hibernate μ΄λ©° λ°λΌμ νΈλμμ μ λ«λ 주체λ Hibernate μΈ κ²μΌλ‘ μ΄μΌκΈ°νλ©° λͺ¨λ λ©μλμ @Transactional μ μ μΈνλ κ²μ κΆμ₯νκ³ μλ€.
Reference
- https://colin-d.medium.com/querydsl-%EC%97%90%EC%84%9C-db-connection-leak-%EC%9D%B4%EC%8A%88-40d426fd4337
- https://github.com/spring-projects/spring-framework/blob/5.3.x/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java
- https://github.com/querydsl/querydsl/issues/3089
'π backend > spring' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
| hibernate.query.in_clause_parameter_padding = true ? (1) | 2024.03.06 |
|---|---|
| JPA μ UUID version 7 μ μ©νκΈ° (0) | 2024.03.04 |
| [Spring] Spring AOP λμ μμ보기 (0) | 2024.03.04 |
| [JPA] μ½λλ‘ λ³΄λ OSIV μ κ³ μ°° (2) | 2024.01.30 |
| ν μ€νΈμμ @Sql λ‘ ν μ€νΈ λ°μ΄ν°κ° λ€μ΄κ°μ§ μμλ μ΄μ (with. custom TestExecutionListener) (1) | 2024.01.24 |
