트랜잭션 경계를 직접 관리할 때 생기는 문제를 스프링이 세 단계로 걷어낸 결과. @Transactional 한 줄이 그 끝에 있다.
경계를 직접 잡을 때 남는 네 가지
순수 JDBC로 하나의 비즈니스 로직을 단일 트랜잭션으로 묶으려면 서비스 로직 시작 전에 트랜잭션을 열고 끝난 뒤에 닫아야 한다. 그렇게 경계를 설정하고 나면 네 가지가 남는다. 서비스 코드가 복잡해지고, 커넥션을 어떻게 유지할지가 숙제가 되고, 데이터 액세스 기술에 의존적인 코드를 쓰게 되고, 비즈니스 로직과 관심사가 다른 일이 그 안에 섞인다.
트랜잭션 동기화
첫 단계는 트랜잭션 동기화다. 순수 JDBC에서는 커넥션을 유지하려고 Connection 객체를 메서드 파라미터로 계속 넘겨야 했다. 스프링은 커넥션을 특별한 저장소에 보관해두고 필요할 때 꺼내 쓰게 했고, 그 저장소를 트랜잭션 동기화 매니저라고 부른다. 스레드마다 커넥션을 독립적으로 관리하므로 멀티스레드 환경에서 충돌하지 않는다.
동작은 이렇다. 서비스에서 트랜잭션이 시작되면 매니저가 커넥션을 만들고 autoCommit을 false로 세팅한 뒤 스레드 로컬에 보관한다. 리포지토리는 파라미터로 받는 대신 그 스레드 로컬에서 커넥션을 꺼내 쓴다. 트랜잭션이 끝나면 매니저에서 커넥션을 가져와 커밋하거나 롤백하고 자원을 정리한 뒤 닫는다. 커넥션 풀을 쓰면 첫 단계에서 꺼내오고 마지막 단계에서 돌려준다.
트랜잭션 추상화
두 번째는 트랜잭션 추상화다. 동기화 코드가 JDBC에 종속적이라는 문제가 남아 있기 때문이다. 데이터 접근 기술마다 다루는 객체가 달라서 JDBC는 Connection, JPA는 EntityManager, Hibernate는 Session을 쓴다. 그런데 셋이 하는 일은 트랜잭션을 가져와 만들고, 커밋하고, 롤백하는 것으로 똑같다. 스프링은 PlatformTransactionManager 인터페이스를 두고 각 기술의 구현체가 그 명세를 따르게 했다. 애플리케이션은 어떤 기술을 쓰든 일관되게 트랜잭션을 처리한다. PSA의 대표적인 예다.
추상화는 POJO에 가까워져 테스트하기 좋다는 장점을 준다. 반대로 코드 계층이 깊어진다는 뜻이기도 해서 오히려 추적이 어려워질 수 있다.
@Transactional
세 번째가 선언적 트랜잭션이다. 여전히 트랜잭션 코드가 비즈니스 로직 사이에 섞여 있다는 마지막 문제를 @Transactional이 해결한다. AOP가 메서드 앞뒤에 트랜잭션 처리를 끼워 넣으므로 추가 코드를 쓰지 않아도 된다.
프로그래밍 방식과 선언적 방식
| 프로그래밍 방식 | 선언적 방식 | |
|---|---|---|
| 어떻게 | TransactionTemplate을 직접 쓴다 | @Transactional을 붙인다 |
| 경계 | 시작과 종료를 직접 정한다 | 메서드 단위 |
| 유연성 | 높다 | 낮다 |
| 양이 많을 때 | 관리하기 힘들다 | 간결하다 |
트랜잭션이 적고 세밀한 제어가 필요하면 프로그래밍 방식이 낫다. 트랜잭션 프록시를 설정하지 않아도 되기 때문이다. 다수의 트랜잭션을 다룰 때는 트랜잭션 관리를 비즈니스 로직에서 빼낼 수 있는 선언적 방식이 유지보수에 유리하다. 실무에서는 대부분 @Transactional을 쓴다.
자기 호출이라는 구멍
AOP 기반이라 같은 클래스 안에서 부르면 트랜잭션이 걸리지 않는다. 왜 그런지와 어떻게 피하는지는 프록시의 한계에 있다.