QueryDSL transform() μ—μ„œ κΌ­ @Transactional 을 μ‚¬μš©ν•΄μ•Ό ν•˜λŠ” 이유

2024. 1. 29. 16:52Β·πŸ“‚ backend/spring

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 μƒνƒœλ₯Ό 디버깅 섀정을 톡해 ν™•μΈν•˜μ˜€λ‹€.

쿼리 싀행이 μ™„λ£Œλ˜μ—ˆμ§€λ§Œ λ°˜ν™˜ν•˜μ§€ μ•ŠλŠ” connection

 

 

μš°μ„  κ²°λ‘ λΆ€ν„° μ΄μ•ΌκΈ°ν•˜μžλ©΄ transform() λ©”μ„œλ“œμ—μ„œ @Transactional 이 μ„ μ–Έλ˜μ–΄ μžˆμ§€ μ•ŠμœΌλ©΄ connection leak 이 λ°œμƒν•œλ‹€.

κ·Έ μ΄μœ λŠ” μ•„λž˜ QueryDSL μ†ŒμŠ€ μ½”λ“œλ₯Ό 톡해 ν™•μΈν•΄λ³΄μž.

 

 

2. QueryDSL λ‚΄λΆ€ μ½”λ“œλ₯Ό ν™•μΈν•΄λ³΄μž.

 

쑰금 더 세뢀적인 결둠은 transform() λ©”μ„œλ“œμ—μ„œ @Transactional 을 μ„ μ–Έν•˜μ§€ μ•ŠμœΌλ©΄ EntityManager λ₯Ό λ‹«νžˆμ§€ μ•Šμ•„ λ°œμƒν•˜λŠ” λ¬Έμ œλ‹€. 이λ₯Ό 확인해보기 μœ„ν•΄ transform() λ©”μ„œλ“œ λ‚΄λΆ€λ₯Ό ν™•μΈν•΄λ³΄μž.

 

transform() λ©”μ„œλ“œλŠ” λ‚΄λΆ€μ μœΌλ‘œ iterate() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”λ° queryHandler.iterate() λ₯Ό ν˜ΈμΆœν•œλ‹€.

그리고 λ‚΄λΆ€μ μœΌλ‘œ SharedEntityManagerCreator.invoke() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”λ° λ‘œμ§μ„ μš”μ•½ν•˜μžλ©΄ μ•„λž˜μ™€ κ°™λ‹€.

 

 

(1) SharedEntityManagerCreator.invoke() μ—μ„œ EntityManager λ₯Ό λ‹«ν˜€μ•Ό ν•œλ‹€.

  1. ν˜„μž¬ νŠΈλžœμž­μ…˜μ— μ°Έμ—¬ν•˜κ³  μžˆλŠ” EntityManager λ₯Ό μ‘°νšŒν•œλ‹€. (μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 μƒˆλ‘œμš΄ EntityManager λ₯Ό μƒμ„±ν•œλ‹€.)
  2. EntityManager 의 λ©”μ„œλ“œλ₯Ό ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œ DeferredQueryInvocationHandler Proxy 객체λ₯Ό μƒμ„±ν•΄μ„œ λ°˜ν™˜ν•œλ‹€.
  3. DeferedQueryInvocationHandler μ€ SharedEntityManagerμ—μ„œ λΉ„νŠΈλžœμž­μ…˜ createQuery()κ°€ 호좜될 λ•Œ ν•΄λ‹Ή 쿼리 객체λ₯Ό μ²˜λ¦¬ν•œλ‹€.
  4. (πŸš¨ν•΅μ‹¬!!) 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 repo] Connection leak when using FetchableQueryBase#transform outside of a transaction #3089

μ‹€μ œ 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
'πŸ“‚ backend/spring' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€
  • JPA 에 UUID version 7 μ μš©ν•˜κΈ°
  • [Spring] Spring AOP λ™μž‘ μ•Œμ•„λ³΄κΈ°
  • [JPA] μ½”λ“œλ‘œ λ³΄λŠ” OSIV 와 κ³ μ°°
  • ν…ŒμŠ€νŠΈμ—μ„œ @Sql 둜 ν…ŒμŠ€νŠΈ 데이터가 λ“€μ–΄κ°€μ§€ μ•Šμ•˜λ˜ 이유 (with. custom TestExecutionListener)
cooper25
cooper25
  • cooper25
    dev cooper
    cooper25
  • 전체
    였늘
    μ–΄μ œ
    • λΆ„λ₯˜ 전체보기 (84)
      • πŸ“‚ backend (34)
        • spring (19)
        • architecture (10)
        • test (5)
      • πŸ“‚ computer-science (5)
      • πŸ“‚ programming-language (12)
        • java (12)
      • πŸ“‚ infra (9)
        • mysql (4)
        • redis (2)
        • message-queue (3)
      • πŸ“‚ cloud (2)
        • aws (2)
      • πŸ“‚ frontend (1)
        • react (0)
      • πŸ“‚ education & lecture (16)
        • 인프라 곡방 (11)
        • ν•­ν”Œ λ°±μ—”λ“œ 7κΈ° (5)
      • πŸ“‚ ai (1)
        • claude (1)
      • πŸ“‚ etc (2)
        • 회고 (1)
        • 컨퍼런슀 (1)
  • λΈ”λ‘œκ·Έ 메뉴

    • ν™ˆ
    • νƒœκ·Έ
    • λ°©λͺ…둝
  • 링크

  • 곡지사항

  • 인기 κΈ€

  • νƒœκ·Έ

    nGrinder
    spring
    AWS
    UUID
    μŠ€ν”„λ§μΊ ν”„ ν›„κΈ°
    mysql
    λ™μ‹œμ„±
    gof
    가상 λ©΄μ ‘ μ‚¬λ‘€λ‘œ λ°°μš°λŠ” λŒ€κ·œλͺ¨ 섀계
    인프라곡방
    ν•­ν•΄ν”ŒλŸ¬μŠ€
    react
    spring AOP
    Redisson
    데이터 쀑심 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 섀계
    회고
    spring camp 2025
    spring-batch
    JPA
    kafka
  • 졜근 λŒ“κΈ€

  • 졜근 κΈ€

  • hELLOΒ· Designed Byμ •μƒμš°.v4.10.6
cooper25
QueryDSL transform() μ—μ„œ κΌ­ @Transactional 을 μ‚¬μš©ν•΄μ•Ό ν•˜λŠ” 이유
μƒλ‹¨μœΌλ‘œ

ν‹°μŠ€ν† λ¦¬νˆ΄λ°”