데이터베이스/JPA

스레드로컬과 세션

정재익 2026. 1. 9. 21:31
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()을 호출해서 스레드로컬에 세션이 담긴다.