@Transactional은 스프링에서 선언적 트랜잭션 관리를 가능하게 해주는 어노테이션이다.
이 어노테이션을 클래스나 메서드에 붙이면, 해당 작업이 하나의 트랜잭션으로 묶여 모든 작업이 성공적으로 완료될 경우 커밋(commit) 되고, 도중에 예외가 발생하면 모든 변경 사항이 자동으로 롤백(rollback) 된다.
횡단 관심사(Cross-Cutting Concerns)를 모듈화하는 AOP에 기반한 처리이면서, 개발자가 직접 트랜잭션을 시작하거나 커밋, 롤백하는 코드를 작성할 필요도 없으니 매우 유용한 기능이다.
@Transactional은 내부적으로 프록시(Proxy) 기술을 활용해 동작한다. 스프링 컨테이너는 @Transactional이 붙은 클래스나 메서드를 발견하면, 실제 객체를 감싸는 프록시 객체를 생성한다.
아래는 @Transactional 메서드 호출 시 작동 순서이다.
메서드 호출 가로채기 - 외부에서 @Transactional이 붙은 메서드를 호출하면, 실제 객체가 아닌 이 프록시 객체가 먼저 호출을 가로챈다.
트랜잭션 인터셉터 - 프록시 내부의 트랜잭션 인터셉터가 어노테이션을 감지하고, 지정된 트랜잭션 매니저(JpaTransactionManager 등)에게 트랜잭션 시작을 요청한다.
트랜잭션 시작 - 트랜잭션 매니저는 데이터 소스로부터 데이터베이스 커넥션을 얻어와 setAutocommit(false)를 설정하고 트랜잭션을 시작한다.
동기화 매니저 등록 - 이 커넥션은 트랜잭션 동기화 매니저에 등록되어, 해당 트랜잭션 안에서 수행되는 모든 데이터베이스 작업이 동일한 커넥션을 사용하도록 보장한다.
모든 작업이 완료되면 인터셉터는 커밋을, 예외가 발생하면 롤백을 수행하도록 제어한다.
프록시 내부 호출 문제 - Service 클래스 내부의 다른 메서드에서 @Transactional이 붙은 메서드를 호출하면, 프록시를 통하지 않고 실제 객체의 메서드를 직접 호출하게 된다. 이 경우 트랜잭션이 적용되지 않는다. 이를 해결하기 위해서는 트랜잭션이 필요한 메서드를 별도의 클래스로 분리해야 한다.
private 메서드 적용 불가 - @Transactional은 public 메서드에만 적용됩니다. 프록시 기술인 CGLIB은 타켓 클래스를 상속받아 프록시를 생성하는데, private에는 자식 클래스에서 접근 불가능하기 때문이다.
@Transactional은 테스트 코드에서도 매우 유용하게 사용된다.
테스트 메서드에 이 어노테이션을 붙이면, 테스트가 끝난 후 데이터베이스의 모든 변경 사항이 자동으로 롤백된다. 덕분에 테스트 간 데이터 충돌을 방지하고, 매번 데이터를 초기화하는 번거로운 작업을 생략할 수 있다.
다만, 이는 실제 서비스 코드와 테스트 환경의 동작 불일치를 야기할 수 있다는 점을 유의해야 한다.

위 사진은 아래에 있는 테스트 코드만 정상적으로 작동하는 케이스다. JPA의 변경 감지는 트랜잭션 안에서만 동작하기 때문에 @Transactional이 없는 실제 서비스 코드에서는 예상치 못한 문제가 발생할 수 있다.
항상 실제 환경을 염두에 두고 테스트를 작성하는 것이 중요하다.
장점
단점