트랜잭션(Transaction)은 논리적인 작업의 단위를 의미한다. 흔히 데이터베이스에서 여러 개의 데이터 조작 작업을 하나의 논리적인 단위로 묶어서 수행할 때, 이 하나의 작업을 트랜잭션이라고 부른다.
트랜잭션은 일상에서도 쉽게 접할 수 있는 개념이다. 시장에서 상인과 소비자가 거래를 한다고 하자. 이때 "거래"가 하나의 작업, 즉 트랜잭션이 된다. 이 트랜잭션에는 다음과 같은 행위들이 포함된다.
소비자
상인
"거래" 트랜잭션은 상인과 소비자 간 여러 행위로 이뤄진다. 만약 소비자가 상인에게 돈을 지불했지만 상인이 돈을 받지 못한 경우, 거래가 정상적으로 성사되었다고 할 수 없다. 즉 트랜잭션은 여러 요소로 구성될 수 있으며, 각각의 요소가 트랜잭션의 논리를 이루게 된다.
데이터베이스 시스템에서는 트랜잭션을 통해 데이터를 일관성 있게 유지하고, 데이터 조작 작업을 안전하게 수행한다.
데이터베이스 시스템에서 트랜잭션은 ACID로 일컬어지는 특징을 갖는다. ACID 개념을 "계좌 이체"를 예시로 이해해보자.
"계좌 이체"를 하나의 트랜잭션으로 볼 때, 이 트랜잭션은 크게 두 개의 작업으로 구성된다. 송금(돈을 보내는 것)과 수취(돈을 받는 것)이다. 데이터베이스에서는 만약 두 작업 중 하나라도 실패하게 되면 트랜잭션의 연산이 모두 반영되지 않고 이전 상태로 복구된다. 예를 들어, 송금이 성공하고 수취가 실패하면 송금된 금액은 이전 상태로 되돌려져 계좌의 잔액 변동이 없게 된다.
이체 트랜잭션이 수행된 이후에는 계좌 간 잔액의 일관성이 유지된다. 예를 들어 A 계좌에서 B 계좌로 일정한 금액을 이체한다고 할 때, 송금된 금액은 A 계좌에서 차감되고, B 계좌에는 해당 금액이 추가된다.
A 계좌에서 B 계좌로 일정 금액을 이체하는 트랜잭션이 다른 트랜잭션과 동시에 실행된다고 가정하자. A 계좌에서 이체 작업이 진행 중일 때, B 계좌에 아직 수취 금액이 반영되지 않았는데도, 다른 트랜잭션이 B 계좌의 잔액을 읽을 경우 부정확한 데이터를 읽게 된다. 이러한 일을 방지하기 위해 여러 트랜잭션은 서로 독립성을 지키게 된다.
이체 트랜잭션이 성공적으로 완료되면 해당 내용은 데이터베이스에 영구적으로 저장된다. 즉, 송금 이후 잔액과 수취로 인해 증감된 금액은, 다른 작업이 없는 경우 영구적으로 남아 있게 된다.
스프링은 @Transactional
어노테이션을 지원한다. @Transactional을 사용하면 트랜잭션을 쉽게 관리할 수 있다.
@Transactional은 메서드 또는 클래스 상단에 적용할 수 있다. 스프링은 @Transactional이 붙은 영역에서 데이터베이스 연산이 수행될 때 트랜잭션을 시작하고, 실행이 정상적으로 완료되면 트랜잭션을 데이터베이스에 반영한다. 이를 커밋(Commit)이라고 한다. 만약 트랜잭션 연산 중 예외가 발생한다면 트랜잭션이 반영되지 않고 이전 상태로 되돌아간다. 이를 롤백(Rollback)이라고 한다.
@Transactional 어노테이션은 다양한 옵션을 설정해, 세밀하게 제어할 수 있다. 주요 설정으로는 'isolation', 'propagation', 'readOnly', 'timeout' 등이 있다. 이러한 설정의 세부사항은 다음에 알아보도록 하자.
DB와 연동된 테스트 코드를 작성할 때 @Transactional을 사용하면 편리하다.
DB와 연동된 테스트 코드는, 여러 테스트가 같은 DB를 사용하기 때문에 어려움이 생길 수 있다. 각각의 테스트 메서드가 서로 영향을 주고 받아 비결정적인(non-deterministic) 테스트가 될 수 있기 때문이다. @Trasactional 어노테이션을 적용하면 이러한 문제에서 벗어날 수 있다.
@Transactional은 각 테스트 메서드를 하나의 트랜잭션으로 다루며, 메서드가 종료될 때마다 DB를 롤백시킨다. 결국 각 테스트 메서드가 서로 영향을 받지 않게 된다.
단, 트랜잭션은 Auto Increment 되는 id를 롤백시키지 않는다. 데이터베이스 엔진이 Auto Increment 값을 트랜잭션과 독립적으로 관리하기 때문이다.
한 프로그램에 여러 사용자가 접근하는 상황을 생각하면 트랜잭션 연산 안에 Auto Increment 값이 포함되지 않는 이유를 이해할 수 있다. 여러 사용자가 동일한 계좌에 이체를 한다고 가정해보자. 이 트랜잭션들은 데이터를 새로 생성하는 연산을 포함할 것이다. 이때 각 트랜잭션이 성공 여부를 확인받은 후에야 id를 부여받게 된다면, 시간이 매우 오래 걸리게 될 것이다. 따라서 Auto Increment id는 트랜잭션과 별도로 관리된다.