티스토리 뷰
Spring 환경에서는 JPA의 더티 체킹이나 Bulk Insert 등에서 성능 이슈로 인해 JdbcTemplate을 혼용해서 사용해야 할 때가 있다.
(MySQL의 PK 전략이 IDENTITY AUTO INCREMENT 인 경우 가정)
이때 우리는 자연스럽게 Service 계층에 @Transactional을 붙이고, 내부에서 jdbcTemplate.update() 등을 호출한다.
근데 여기서 이런 의문이 든다.
"나는 JdbcTemplate 메서드에 DB Connection을 넘겨준 적이 없는데, 어떻게 기존 트랜잭션에 참여해서 동작하는 걸까?"
Spring이 어떻게 트랜잭션 컨텍스트를 유지하고 전파하는지, 그 핵심 원리인 트랜잭션 동기화(Transaction Synchronization) 에 대해 정리해보자.
1. JdbcTemplate은 상태를 가지지 않는다 (Stateless)
먼저 JdbcTemplate의 특징을 이해해야 한다. 보통 JdbcTemplate은 스프링 빈으로 등록하여 싱글톤(Singleton)으로 관리된다.
@Configuration
DataSourceConfiguration 클래스 내부
@Bean
@Primary
fun mysqlJdbcTemplate(mysqlDataSource: DataSource): JdbcTemplate {
return JdbcTemplate(mysqlDataSource)
}
@Repository
class MembershipRepository(
private val membershipJpaRepository: MembershipJpaRepository,
private val jdbcTemplate: JdbcTemplate, <<< 싱글톤스코프 Bean 주입
) : QuerydslRepositorySupport(Membership::class.java),
MembershipJpaRepository by membershipJpaRepository {
@Transactional
fun saveMembershipBatchInsert(membershipList: List<Membership>, defaultLevelId: Long) {
jdbcTemplate.batchUpdate(
/// INSERT ...
)
}
}
만약 JdbcTemplate이 내부 멤버 변수에 Connection을 저장하고 사용한다면, 멀티 스레드 환경에서 심각한 동시성 이슈가 발생할 것이다.
하지만 JdbcTemplate은 Thread-Safe하다. 자체적으로는 상태(Connection 정보)를 가지지 않기 때문이다.
그렇다면 트랜잭션 중인 커넥션은 도대체 어디에 있는거고 누가 관리해주고 있는걸까?
2. TransactionSynchronizationManager
바로 Spring의 TransactionSynchronizationManager 와 ThreadLocal에 트랜잭션과 커넥션에 관련된 정보가 담겨있다.
Spring 트랜잭션 관리의 핵심 흐름은 아래와 같다.
- 트랜잭션 시작 (@Transactional)
- AOP가 개입하여 트랜잭션 매니저를 통해 커넥션을 생성하고 setAutoCommit(false)로 트랜잭션 시작. - 커넥션 동기화 (Binding)
- 생성된 커넥션을 TransactionSynchronizationManager를 통해 ThreadLocal에 저장.
- ThreadLocal은 스레드마다 할당된 고유한 저장소이므로, 다른 스레드의 간섭 없이 안전하게 보관된다. - JdbcTemplate 실행
- jdbcTemplate.update()가 호출된다.
3. JdbcTemplate이 커넥션을 가져오는 방법: DataSourceUtils
JdbcTemplate 내부 코드를 들여다보면, 단순히 DataSource.getConnection()을 호출해서 새 커넥션을 맺는 것이 아니다.
스프링의 유틸리티 클래스인 DataSourceUtils 를 사용하여 스레드 로컬에 접근하여 커넥션 정보를 가져오게 되어있다.
@Nullable
private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources)
throws DataAccessException {
// 생략
Connection con = DataSourceUtils.getConnection(obtainDataSource());
try {
// 쿼리 생략
}
catch (SQLException ex) {
// 자원 정리
}
finally {
if (closeResources) {
// 생략
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
// 생략
}
<TransactionSynchronizationManager 클래스 내부>
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
return doGetResource(actualKey);
}
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
if (value instanceof ResourceHolder resourceHolder && resourceHolder.isVoid()) {
map.remove(actualKey);
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
코드 작동 원리를 정리하면 다음과 같다.
- JdbcTemplate이 쿼리를 실행하기 전 DataSourceUtils.getConnection(dataSource)를 호출한다.
- DataSourceUtils는 먼저 TransactionSynchronizationManager를 확인한다.
- 현재 스레드(ThreadLocal)에 바인딩된 커넥션이
- 있다면? → 그 커넥션을 반환한다. (즉, 여기서 트랜잭션 참여가 이뤄짐)
- 없다면? → 그때야 비로소 새로운 커넥션을 생성함.
4. 전체 흐름 요약
이 과정을 정리해서 트랜잭션의 흐름을 도식화하면 아래와 같다.
Service (@Transactional) ⬇️
TransactionManager: 커넥션 생성 & 트랜잭션 시작 ⬇️
TransactionSynchronizationManager: 커넥션을 ThreadLocal에 보관 ⬇️
Repository (JdbcTemplate): 쿼리 실행 요청 ⬇️
DataSourceUtils: ThreadLocal에서 보관된 커넥션 조회 및 반환 ⬇️
SQL 실행: (동일한 커넥션 사용으로 트랜잭션 유지)
5. 결론
결국 JdbcTemplate이 멀티 스레드 환경에서도 안전하고, 별도의 커넥션 파라미터 전달 없이도 트랜잭션에 참여할 수 있는 이유는 다음과 같다.
- Stateless: JdbcTemplate 자체는 상태를 가지지 않는다.
- ThreadLocal: 트랜잭션 리소스(Connection)는 TransactionSynchronizationManager의 ThreadLocal에 스레드별로 격리되어 관리된다.
- DataSourceUtils: JdbcTemplate은 내부적으로 동기화 매니저를 조회하여 리소스를 재사용하는 DataSourceUtils라는 유틸리티를 사용한다.
이러한 원리 덕분에 JPA와 JDBC를 혼용하더라도, 하나의 @Transactional 안에서 원자성(Atomic)을 보장받으며 데이터를 안전하게 처리할 수 있다.
참고 자료
- Spring Framework Documentation
- JPA -> JDBC 전환 경험 및 원리 학습 내용 기반
'Web > 정리글' 카테고리의 다른 글
| JWT 사용이유 (0) | 2022.05.26 |
|---|---|
| SYN Flooding Attack(DDos) 대응 정리 (0) | 2021.12.27 |
| 자바 웹 역사 정리 (1) | 2021.12.21 |
| 멀티 쓰레드 정리 (0) | 2021.12.20 |
| HTTPS, TLS 정리 (0) | 2021.10.19 |
- Total
- Today
- Yesterday