public class ThreadLocalSessionContext extends AbstractCurrentSessionContext {
private static final ThreadLocal<Map<SessionFactory,Session>> CONTEXT_TL = ThreadLocal.withInitial( HashMap::new );
하이버네이트의 스레드로컬 세션 콘텍스트 클래스
스레드로컬은 맵으로 이루어져있으며 세션팩토리를 키로 많은 세션이 들어있다.
@Override
public Session getCurrentSession() {
if ( currentSessionContext == null ) {
throw new HibernateException( "No CurrentSessionContext configured" );
}
return currentSessionContext.currentSession();
}
세션 팩토리에서 세션을 조회하면
@Override
public final Session currentSession() throws HibernateException {
Session current = existingSession( factory() );
if ( current == null ) {
current = buildOrObtainSession();
// register a cleanup sync
current.getTransaction().registerSynchronization( buildCleanupSynch() );
// wrap the session in the transaction-protection proxy
if ( needsWrapping( current ) ) {
current = wrap( current );
}
// then bind it
doBind( current, factory() );
}
else {
validateExistingSession( current );
}
return current;
}
스레드로컬세션콘텍스트클래스에서 이 메서드가 호출되어 doBind를 통해 스레드로컬에 세션이 바인딩 된다.
protected Session wrap(Session session) {
final var wrapper = new TransactionProtectionWrapper( session );
final var wrapped = (Session) Proxy.newProxyInstance(
Session.class.getClassLoader(),
SESSION_PROXY_INTERFACES,
wrapper
);
// yuck! need this for proper serialization/deserialization handling...
wrapper.setWrapped( wrapped );
return wrapped;
}
private class TransactionProtectionWrapper implements InvocationHandler, Serializable {
private final Session realSession;
private Session wrappedSession;
public TransactionProtectionWrapper(Session realSession) {
this.realSession = realSession;
}
스레드로컬에는 세션이 아닌 세션의 참조가 담긴다. 래퍼에는 자기자신
wrapped에는 프록시가 담김
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);
/*
* Look up or generate the designated proxy class and its constructor.
*/
Constructor<?> cons = getProxyConstructor(loader, interfaces);
return newProxyInstance(cons, h);
}
JDK 동적 프록시를 이용
세션을 가지고 있는 프록시가 바인딩 되는것 그래서 세션은 스레드안전하지 않다. 프록시는 세션을 가지고 있다.
protected static class CleanupSync implements Synchronization, Serializable {
protected final SessionFactory factory;
public CleanupSync(SessionFactory factory) {
this.factory = factory;
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int i) {
unbind( factory );
}
}
트랜잭션끝나면 afterCompletion으로
@Override
@SuppressWarnings("SimplifiableIfStatement")
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final String methodName = method.getName();
// first check methods calls that we handle completely locally:
if ( "equals".equals( methodName ) && method.getParameterCount() == 1 ) {
if ( args[0] == null
|| !Proxy.isProxyClass( args[0].getClass() ) ) {
return false;
}
return this.equals( Proxy.getInvocationHandler( args[0] ) );
}
else if ( "hashCode".equals( methodName ) && method.getParameterCount() == 0 ) {
return hashCode();
}
else if ( "toString".equals( methodName ) && method.getParameterCount() == 0 ) {
return String.format( Locale.ROOT,
"ThreadLocalSessionContext.TransactionProtectionWrapper[%s]",
realSession );
}
// then check method calls that we need to delegate to the real Session
try {
// If close() is called, guarantee unbind()
if ( "close".equals( methodName ) ) {
unbind( realSession.getSessionFactory() );
CURRENT_SESSION_LOGGER.allowingInvocationToProceed(methodName);
}
else if ( "getStatistics".equals( methodName )
|| "isOpen".equals( methodName )
|| "getListeners".equals( methodName ) ) {
// allow these to go through the real session no matter what
CURRENT_SESSION_LOGGER.allowingInvocationToProceed(methodName);
}
else if ( !realSession.isOpen() ) {
// essentially, if the real session is closed, allow any method
// call to pass through since the real session will complain by
// throwing an appropriate exception; note that allowing close()
// above has the same basic effect, but we capture that there
// just to unbind().
CURRENT_SESSION_LOGGER.allowingInvocationToProceedToClosedSession(methodName);
}
else if ( realSession.getTransaction().getStatus() != TransactionStatus.ACTIVE ) {
// limit the methods available if no transaction is active
if ( "beginTransaction".equals( methodName )
|| "getTransaction".equals( methodName )
|| "isTransactionInProgress".equals( methodName )
|| "setFlushMode".equals( methodName )
|| "setHibernateFlushMode".equals( methodName )
|| "getFactory".equals( methodName )
|| "getSessionFactory".equals( methodName )
|| "getJdbcCoordinator".equals( methodName )
|| "getTenantIdentifier".equals( methodName ) ) {
CURRENT_SESSION_LOGGER.allowingInvocationToProceedToNonTransactedSession(methodName);
}
else {
throw new HibernateException( "Calling method '" + methodName
+ "' is not valid without an active transaction (Current status: "
+ realSession.getTransaction().getStatus() + ")" );
}
}
return method.invoke( realSession, args );
}
catch ( InvocationTargetException e ) {
if (e.getTargetException() instanceof RuntimeException) {
throw e.getTargetException();
}
throw e;
}
}
프록시로 진짜 세션을 찾아 언바인드 시킴
private static Session doUnbind(SessionFactory factory) {
final var sessionMap = sessionMap();
final var session = sessionMap.remove( factory );
if ( sessionMap.isEmpty() ) {
//Do not use set(null) as it would prevent the initialValue to be invoked again in case of need.
CONTEXT_TL.remove();
}
return session;
}
맵에서 세션을 정리하는것을 볼 수 있다.
참고로 @Transactional을 사용하면 AOP가 메서드 진입시 자동으로 getCurrentSession()을 호출해서 스레드로컬에 세션이 담긴다.
'데이터베이스 > JPA' 카테고리의 다른 글
| 지연로딩 vs 즉시로딩 vs 패치조인 vs JPQL일반조인 vs 일반조인 (0) | 2026.01.12 |
|---|---|
| 세션과 커넥션 (1) | 2026.01.09 |