JPA, JDBC 등 데이터 접근 방식마다 같은 기능을 다르게 구현하고 있기 때문에 데이터 접근 방식 변경 시 여러 코드에 변경 사항이 전파됐다.
이를 해결하기 위해 PlatformTransactionManager 인터페이스로 추상화해서 여러 데이터 접근 기술을 쉽게 변경 가능하도록 만들었다.
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야 한다.
이전에는 파라미터로 커넥션을 전달하면서 데이터베이스 커넥션을 유지했지만, 이러한 방식은 코드가 지저분해지는 등 여러 가지 단점이 많았다.
각각의 스레드마다 별도의 저장소가 부여되기 때문에 특정 스레드만 해당 데이터에 접근할 수 있어서 멀티 스레드 환경에서 안전하게 커넥션을 보관할 수 있다.
이러한 성질을 이용해 스레드 로컬을 사용해서 데이터베이스 커넥션을 동기화한다.
transactionManager.getTransaction()을 호출해서 트랜잭션을 시작한다.transactionManager.getTransaction() 동작 과정
데이터 소스를 통해서 DB 커넥션을 획득한다.
해당 커넥션을 수동 커밋 모드로 변경해서 실제 DB 트랜잭션을 시작한다.
커넥션을 트랜잭션 동기화 매니저에게 전달한다.
트랜잭션 동기화 매니저는 전달받은 커넥션을 스레드 로컬에 보관한다.
💡 데이터 소스(데이터베이스 커넥션 풀)
데이터 소스에서 데이터베이스 커넥션을 관리한다.
데이터베이스 커넥션을 생성하는 비용이 크기 때문에 매번 커넥션을 생성하는 것은 비효율적이다. 이를 해결하기 위해서 커넥션을 미리 확보해서 스레드 풀과 같은 커넥션 풀에 보관(기본 값은 10개)한다.
애플리케이션은 매번 커넥션을 생성하지 않고, 미리 생성해둔 커넥션을 커넥션 풀에서 꺼내 쓰고 사용 후에 반환하면 된다. 이를 통해서 커넥션을 생성하는 비용을 줄일 수 있고, 무한정으로 DB에 커넥션이 연결되는 것을 막으면서 DB를 보호하는 효과도 있다.
서비스는 비즈니스 로직을 실행하면서 레포지토리 메서드를 호출한다.
레포지토리 메서드는 SQL을 데이터베이스에 전달하기 위해 커넥션이 필요하다. 이때, DataSourceUtils.getConnection()을 사용해 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 이 과정으로 자연스럽게 동일한 커넥션을 사용하면서 트랜잭션도 유지된다.
커넥션을 획득하고, SQL을 DB에 전달해서 실행한다.
transactionManager.commit() 또는 rollback()을 실행한다.transactionManager.commit() 또는 rollback() 동작 과정
트랜잭션 AOP 프록시가 커밋 또는 롤백을 통해 트랜잭션을 종료하기 위해서 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
획득한 커넥션을 통해 트랜잭션을 커밋 또는 롤백한다.
스레드 로컬 등과 같이 사용한 모든 리소스를 정리한다.
1번부터 12번 과정을 다음 코드와 같이 만들 수 있다.
@Service
class Service(private val dataSource: DataSource) {
fun dataSource(ticketId: Long, name: String) {
// 2
val connection = dataSource.connection
// 3
connection.autoCommit = false
try {
// 6~8
bussinessLogic()
// 11
connection.commit()
} catch (e: Exception) {
// 11
connection.rollback()
} finally {
// 12
connection.close()
}
}
}
데이터 소스를 주입 한다.
데이터 소스로부터 DB 커넥션을 획득한다.
autoCommit = false를 통해서 수동 커밋 모드로 변경한다.
try catch 구문 안에서 비즈니스 로직을 실행하고, 오류 없이 마무리 되면 트랜잭션을 커밋한다.
만일 오류가 발생하면 트랜잭션을 롤백한다.
커밋 또는 롤백을 한 뒤, connection.close()를 통해서 커넥션을 반납한다.
DB 커넥션 획득 및 반납, 수동 커밋 모드 설정을 별도로 안 해도 돼서 DataSource 방식 보다 비교적 편하게 트랜잭션을 관리할 수 있다.
@Service
class Service(private val transactionManager: PlatformTransactionManager) {
fun transactionManager(ticketId: Long, name: String) {
// 1 ~ 6
val status = transactionManager.getTransaction(DefaultTransactionDefinition())
try {
bussinessLogic()
// 9 ~ 12
transactionManager.commit(status)
} catch (e: Exception) {
// 9 ~ 12
transactionManager.rollback(status)
}
}
}
transactionManager.getTransaction() 을 통해서 DB 커넥션을 획득한다.
비즈니스 로직을 실행한다.
트랜잭션을 커밋 또는 롤백한다.
TransactionTemplate은 DB 커넥션 획득 및 반납, 커밋 또는 롤백을 별도로 안 해도 된다.
@Service
class Service(transactionManager: PlatformTransactionManager) {
private val transactionTemplate = TransactionTemplate(transactionManager)
fun transactionTemplate(ticketId: Long, name: String) {
transactionTemplate.execute {
bussinessLogic()
}
}
}
이렇게 총 3가지 프로그래밍 방식의 트랜잭션 관리 방법을 알아 보았다.
하지만 프로그래밍 방식의 트랜잭션 관리는 비즈니스 로직에 트랜잭션 관리 로직이 추가되어 순수한 비즈니스 로직만 남길 수 없다는 단점이 있다.
이를 선언적 트랜잭션 관리 방법을 통해 보완할 수 있다.
선언적 트랜잭션 관리 방법은 스프링 AOP를 이용해서 트랜잭션을 관리하는 방법이다.

@Transactional 어노테이션을 사용해서 DB 커넥션을 선언적으로 연결할 수 있고, 이로 인해 비즈니스 로직에서 트랜잭션 관련 로직을 제거하고, 순수한 비즈니스 로직만 남길 수 있다.
@Service
class Service() {
@Transactional
fun transactionTemplate(ticketId: Long, name: String) {
bussinessLogic()
}
}
프록시를 이용해서 트랜잭션을 관리하기 때문에 private 함수에 적용 불가능 , 내부 함수 호출 불가능 등을 주의해서 사용해야 한다.
이번 기회에 스프링 트랜잭션 특징, 동작 과정, 프로그래밍 방식의 트랜잭션 관리, 선언적 방식의 트랜잭션 관리에 대해 학습할 수 있었다.
이후에 트랜잭션 범위를 임의로 설정하고 싶지만 선언적 방식으로 관리가 힘들 때, 프로그래밍 방식으로 트랜잭션을 관리하는 것을 시도해볼 것 같다.
참고
스프링 DB 1편 - 데이터 접근 핵심 원리
Programmatic Transaction Management
Understanding the Spring Framework’s Declarative Transaction Implementation