ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @Transaction를 적용하면 어떻게 하나의 Connection을 사용할까?
    백엔드/Spring 2023. 4. 14. 00:11
    반응형

    자동차 경주 웹 미션을 하는데 페어가 JdbcTemplate update를 여러 번 호출하면 @Transaction 을 적용해도 Connection이 여러번 열리고 닫히는지 궁금해했다.

     

    그래서 JdbcTemplate update 내부 코드를 보니까 update를 할 때마다 Connection이 한 번 열리고 닫히는 것 같아보였다. 그래서 집에 돌아와 @Transaction을 적용하면 어떻게 달라지는지 내부 코드를 보고 정리를 해보았다.

     

    먼저 JdbcTemplate의 update 코드를 확인해보자

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Override
    public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder)
            throws DataAccessException {
        Assert.notNull(generatedKeyHolder, "KeyHolder must not be null");
        logger.debug("Executing SQL update and returning generated keys");
        return updateCount(execute(psc, ps -> {
            int rows = ps.executeUpdate();
            List<Map<String, Object>> generatedKeys = generatedKeyHolder.getKeyList();
            generatedKeys.clear();
            ResultSet keys = ps.getGeneratedKeys();
            if (keys != null) {
                try {
                    RowMapperResultSetExtractor<Map<String, Object>> rse =
                            new RowMapperResultSetExtractor<>(getColumnMapRowMapper(), 1);
                    generatedKeys.addAll(result(rse.extractData(keys)));
                } finally {
                    JdbcUtils.closeResultSet(keys);
                }
            }
            if (logger.isTraceEnabled()) {
                logger.trace("SQL update affected " + rows + " rows and returned " + generatedKeys.size() + " keys");
            }
            return rows;
        }, true));
    }
    cs

     

    return updateCount(execute(psc, ps -> {....})); 에서 execute 메서드 코드를 확인하자

    updateCount 메서드는 update 쿼리를 실행했을 때 정상적으로 작동이 됐는지 확인하는 메서드이다.

     

    execute 메서드는 오버로딩되어 여러 개가 존재하는데, DataSourceUtils.getConnection이 있는 아무런 execute 메서드를 찾아보면 된다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @Nullable
    private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources)
            throws DataAccessException {
     
        Assert.notNull(psc, "PreparedStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
        if (logger.isDebugEnabled()) {
            String sql = getSql(psc);
            logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
        }
     
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        PreparedStatement ps = null;
        try {
            ps = psc.createPreparedStatement(con);
            applyStatementSettings(ps);
            T result = action.doInPreparedStatement(ps);
            handleWarnings(ps);
            return result;
        }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer) psc).cleanupParameters();
            }
            String sql = getSql(psc);
            psc = null;
            JdbcUtils.closeStatement(ps);
            ps = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw translateException("PreparedStatementCallback", sql, ex);
        }
        finally {
            if (closeResources) {
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                JdbcUtils.closeStatement(ps);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
    }
    cs

     

    여기서 Connection con = DataSourceUtils.getConnection(obtainDataSource());를 통해 Connection을 가지고 오는 것을 확인할 수 있다.

    obtainDataSource()는 getter라서 DataSourceUtils.getConnection() 메서드를 확인하면 된다.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            return doGetConnection(dataSource);
        }
        catch (SQLException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
        }
        catch (IllegalStateException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
        }
    }
    cs

     

    doGetConnection(dataSource)를 확인

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
     
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            return conHolder.getConnection();
        }
        // Else we either got no holder or an empty thread-bound holder here.
     
        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = fetchConnection(dataSource);
     
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            try {
                // Use same Connection for further JDBC actions within the transaction.
                // Thread-bound object will get removed by synchronization at transaction completion.
                ConnectionHolder holderToUse = conHolder;
                if (holderToUse == null) {
                    holderToUse = new ConnectionHolder(con);
                }
                else {
                    holderToUse.setConnection(con);
                }
                holderToUse.requested();
                TransactionSynchronizationManager.registerSynchronization(
                        new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }
            }
            catch (RuntimeException ex) {
                // Unexpected exception from external delegation call -> close Connection and rethrow.
                releaseConnection(con, dataSource);
                throw ex;
            }
        }
     
        return con;
    }
    cs

     

    TransactionSynchronizationManager.getResource(dataResource)가 현재 스레드에 바인딩된 DataSource를 가져오는 것이다.

    여러 개의 update문이 들어올 때에는 어떻게 같은 Connection을 반환하도록 하는 것이냐면 ConnectionHolder 때문이다.

     

    1) 처음 update문이 들어온다면 현재 스레드에 바인딩된 DataSource가 없기 때문에 canHolder = TransactionSynchronizationManager.getResource(dataResource) = null을 가지게 된다.

    이로 인해 5줄의 조건문을 건너뛰게 되고, fetchConnection(dataSource)를 통해 하나의 Connection을 열어둔다.

    이후 18줄의 조건문에 다다르게 된다.

    여기서는 holderToUser = canHolder = null이기 때문에 holderToUse = new ConnectionHolder(con)를 통해 holderToUse에 Connection을 저장하고, register를 통해 트랜잭션과 커넥션을 동기화한다.

     

    2) 두번째 update문부터는 TransactionSynchronizationManager.getResource(dataResource)를 하면 이전에 register로 트랜잭션과 커넥션을 동기화했으므로 canHolder에 Connection 존재하게 된다. 그래서 5번 라인의 조건문 내부 코드로 들어가 return canHolder.getConnection()을 통해 같은 Connection을 반환한다.

     

    이런 순서로 동작하게 되어 하나의 Connection가 열리는 것을 보장하게 된다.

     

    정리하면 @Transactional이 들어가게 될 경우, 처음 update 메서드 호출의 doGetConnection은 코드 전체를 실행하지만, 두번째 update 메서드 호출은 같은 Connection을 반환하기 때문에 11줄까지만 실행된다.

    반응형

    댓글

Designed by Tistory.