spring 트랜잭션 전파 속성 - 중첩 트랜잭션 구조의 동작 방식
트랜잭션
트랜잭션이란 데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위이다.
트랜잭션은 데이터를 일관되고 정확하게 관리하기 위해 아래와 같은 특성을 갖는다.
트랜잭션의 특성 - ACID
원자성(Atomic)
- 트랜잭션 내의 모든 작업이 성공하거나 실패하는 것을 보장함
- commit과 rollback을 통해 트랜잭션 내의 모든 연산이 모두 반영되거나 모두 실패시키는 것을 의미함
일관성(Consistency)
- 트랜잭션 완료 이후 데이터베이스이 일관된 상태를 유지함
- 데이터 베이스의 제약조건을 준수하는 것을 의미함
고립성(Isolation)
- 트랜잭션이 다른 트랜잭션의 영향을 받지 않고 독립적으로 실행되는 것을 보장함
- 트랜잭션 작업 수행 중 다른 트랜잭션의 데이터 변경사항에 영향을 받지 않음(이는 DB 고립성 수준에 따라 달라짐)
지속성(Durability)
트랜잭션의 결과가 데이터베이스에 영구적으로 반영된다.
트랜잭션 전파 속성
트랜잭션의 전파속성이란 중첩된 트랜잭션 구조가 나왔을 때, 어떻게 동작시킬지를 결정하는 속성이다.
DB수준에서 중첩 트랜잭션이라는 것은 존재하지 않는다. 부분적으로 sql구문을 롤백시킬수는 있으나 중첩 트랜잭션은 존재하지 않는다.
프로그래밍 코드에서 @Transactional과 같은 어노테이션을 적용하면 중첩된 구조가 나타날 수 있다.
따라서 앞으로 설명한 용어에서 부모-자식 트랜잭션이라는 표현은 프로그래밍 서버 기준이며 실제 DB에서 부모-자식 트랜잭션이라는 것은 존재하지 않는다. 따라서 용어에 혼동에 주의하자.
DB 트랜잭션
[중첩 트랜잭션 미지원]
start transaction;
insert into tx_test values (1, "aaa");
start transaction;
insert into tx_test values (2, "aaa");
rollback;
rollback;
위 쿼리의 결과로 id가 1인 row가 추가될 것이다. 두번째 start transaction을 사용하며 첫번째 트랜잭션이 auto commit된다.
사실상 마지막에 있는 rollback 구문은 의미가 없는 것이다.
DB 수준에서 중첩 트랜잭션은 지원되지 않는다.
[savepoint를 통한 부분 롤백]
start transaction;
insert into tx_test values (1, "aaa");
SAVEPOINT t1; -- 여기까지 저장
insert into tx_test values (2, "bbb");
SAVEPOINT t2; -- 여기까지 저장
rollback to t1; -- t1 밑으로 롤백
commit;
savepoint는 트랜잭션 내부에서 부분 롤백을 지원하는 sql 구문이다.
이 구문을 사용하면 중첩 트랜잭션처럼 동작시킬 수 있다.
위에서 SAVEPOINT t1; 을 통해 트랜잭션 내부에 이름을 선언하고, rollback시 해당 이름을 사용하면 부분 롤백을 할 수 있다.
허나, commit to [save point 이름]이라는 옵션은 없다. save point 여러개 지정하여 rollback을 여러번 수행할 수 있으나 commit은 단 한번 수행에 모든 구문을 반영한다.
즉, db 수준에서 savepoint를 통해 중첩 트랜잭션처럼 만들어 사용하더라도 내부 구문을 롤백하고 외부 구문을 커밋할 수 있으나, 내부 구문을 커밋하고 외부 구문을 롤백할수는 없다
Spring 트랜잭션
Sping에서는 중첩 트랜잭션을 지원하는지 알아보기 위해 spring에서 제공하는 트랜잭션 전파 속성을 살펴보겠다.
전파속성 | 설명 |
REQUIERED | - 부모 트랜잭션 존재한다면 합류, 그렇지 않으면 새로운 트랜잭션 생성 - 부모나 자식 예외시 둘다 롤백 |
SUPPORTS | 트랜잭션을 필요로 하지 않지만 진행 중인 트랜잭션이 존재하면 트랜잭션 사용 (트랜잭션 미 존재시에도 메소드 정상 동작) |
MANDATORY | 부모 트랜잭션에 합류(부모 트랜잭션 미존재시 예외 발생) |
REQUIRES_NEW | 무조건 새로운 트랜잭션 생성(nested한 방식이라도 롤백은 각각 이루어짐) |
NOT_SUPPORTED | 트랜잭션 중단(예외 발생은 안함) |
NEVER | 트랜잭션 사용 안함(진행 중 트랜잭션 존재시 예외 발생) |
NESTED | - REQUIRED 처럼 동작 - But, 자식 예외 부모에 영향 안줌(부모 예외시에만 자식까지 롤백) |
REQUIERED는 spring의 기본 트랜잭션 전파 속성으로 부모와 자식 트랜잭션을 하나의 트랜잭션으로 사용하는 구조이다.
하나의 트랜잭션으로 합쳐진다는 표현이 마치 트랜잭션을 새롭게 생성하고 두개를 합친다는 것처럼 느껴지지만,
프로그래밍 코드에서는 @Transactional을 적용한 메서드가 중첩된 구조를 갖추고 있지만, DB 수준에서는 가장 첫번째 선언된 @Transactional에 대해서만 Start transaction;을 선언하고 이후 @Transactional은 무시된다.
REQUIRES_NEW의 경우 무조건 새로운 트랜잭션 생성이다. 중첩 구조이다.
DB에서는 중첩 트랜잭션이 없기에 SQL구문을 통해 동작시키는 것은 아니며 spring에서 커넥션을 새롭게 할당하여 별개의 트랜잭션으로 동작시킨다. 별개의 커넥션에서 각각 트랜잭션을 수행하기에 프로그래밍단에서 부모 트랜잭션은 롤백되고 자식 트랜잭션은 커밋되는 구조를 만들 수 있다.
NESTED는 내부 구문이 외부 구문에 영향을 주지 않는 구조이다.
DB수준에서 savepoint를 통해 동작시킬 수 있는 구조이다. 실제로 JDBC의 경우 NESTED 옵션을 savepoint를 통해 동작시킨다.
(JPA에서는 NESTED 옵션 미지원된다.)
savepoint를 사용하기에 부모 트랜잭션이 롤백되고 자식 트랜잭션은 커밋되는 구조는 존재하지 않는다.
SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER는 중첩 구조와는 상관 없이 트랜잭션 존재여부에 따라 예외를 발생시키거나 트랜잭션 사용 여부를 결정하지를 정하는 옵션이다.
사실상 DB수준에서 제공하지 않는 중첩 트랜잭션 옵션은 REQUIRES_NEW와 NESTED 옵션 두가지이다.