데이터베이스의 상태를 변화시키는 작업의 단위를 트랜잭션이라고 한다.
원자성(Atomicity)
일관성(Consistency)
고립성(Isolation) → Isolation level의 default 값을 알고 계시나요?
지속성(Durability)
스프링에서 어노테이션 방식(@Transactional
)으로 선언적 트랜잭션 처리를 지원한다.
트랜잭션 적용 범위에서 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit, rollback을 해준다.
REQUIRED(DEFAULT) | 이미 진행중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 진행중이 아니라면 새로운 트랜잭션을 생성한다. |
---|---|
REQUIRED_NEW | 항상 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션이 있다면 잠깐 보류하고 해당 트랜잭션 작업을 먼저 진행한다. |
SUPPORT | 이미 진행중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 없다면 트랜잭션을 설정하지 않는다. |
NOT_SUPPORT | 이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행한다. |
MANDATORY | 이미 진행중인 트랜잭션이 있어야만, 작업을 수행한다. 없다면 Exception을 발생시킨다. |
NEVER | 트랜잭션이 진행중이지 않을 때 작업을 수행한다. 트랜잭션이 있다면 Exception을 발생시킨다. |
NESTED | 진행중인 트랜잭션이 있다면 중첩된 트랜잭션이 실행되며, 존재하지 않으면 REQUIRED와 동일하게 실행된다. |
A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꿈
아직 커밋하지 않음
B 트랜잭션에서 10번 사원의 나이를 조회함
28살이 조회됨
이를 더티 리드(Dirty Read)라고 한다
A 트랜잭션에서 문제가 발생해 ROLLBACK
B 트랜잭션은 10번 사원이 여전히 28살이라고 생각하고 로직을 수행함
B 트랜잭션에서 10번 사원의 나이를 조회
27살이 조회됨(Undo 영역에 백업된 데이터를 조회)
A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꾸고 커밋
B 트랜잭션에서 10번 사원의 나이를 다시 조회(변경되지 않은 이름이 조회됨)
28살이 조회됨
정합성 문제가 해결된 것처럼 보이지만, 하나의 트랜잭션내에서 똑같은 SELECT를 수행했을 경우 항상 같은 결과를 반환해야 한다는 REPEATABLE READ 정합성에 어긋나는 것이다.
REPEATABLE_READ (level 2)
트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준
MYSQL에서 기본으로 사용하고 있고, 해당 격리수준에서는 NON-REPEATABLE READ 부정합이 발생하지 않는다.
MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽게 된다. Undo 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 포함되어 있다.
UPDATE 부정합
START TRANSACTION; -- transaction id : 1
SELECT * FROM Member WHERE name='junyoung';
START TRANSACTION; -- transaction id : 2
SELECT * FROM Member WHERE name = 'junyoung';
UPDATE Member SET name = 'joont' WHERE name = 'junyoung';
COMMIT;
UPDATE Member SET name = 'zion.t' WHERE name = 'junyoung'; -- 0 row(s) affected
COMMIT;
이 상황에서 최종 결과는 name = joont
가 된다.
REPETABLE READ이기 때문에,
2번 트랜잭션에서 name = joont
로 변경하고 COMMIT을 하면 name = junyoung
의 내용을 Undo 영역에 남겨놔야 한다.
그래야 1번 트랜잭션에서 일관되게 데이터를 보는 것을 보장해줄 수 있기 때문이다.
이 상황에서 아래 구문에서 UPDATE 문을 실행하게 되는데, UPDATE의 경우 변경을 수행할 로우에 대해 잠금이 필요하다
하지만 현재 1번 트랜잭션이 바라보고 있는 name = junyoung
의 경우 레코드 데이터가 아닌 Undo 영역의 데이터이고, Undo 영역에 있는 데이터에 대해서는 쓰기 잠금을 걸 수가 없다.
그러므로 위의 UPDATE 구문은 레코드에 대해 쓰기 잠금을 시도하려고 하지만 name = junyoung
인 레코드는 존재하지 않으므로, 0 row(s) affected
가 출력되고, 아무 변경도 일어나지 않게 된다.
그러므로 최종적으로 결과는 name = joont
가 된다. 자이언티가 되지 못해 아쉽다.
Phantom READ
한 트랜잭션 내에서 같은 쿼리를 두 번 실행했는데, 첫 번째 쿼리에서 없던 유령(Phantom) 레코드가 두 번째 쿼리에서 나타나는 현상을 말한다.
REPETABLE READ 이하에서만 발생하고(SERIALIZABLE은 발생하지 않음), INSERT에 대해서만 발생한다.
런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.
트랜잭션 작업 중 런타임 예외가 발생하면 롤백한다. 반면에 예외가 발생하지 않거나 체크 예외가 발생하면 커밋한다.
체크 예외를 커밋 대상으로 삼는 이유는 체크 예외가 예외적인 상황에서 사용되기 보다는 리턴 값을 대신해서 비즈니스 적인 의미를 담은 결과로 돌려주는 용도로 사용되기 때문이다.
스프링에서는 데이터 엑세스 기술의 예외를 런타임 예외로 전환해서 던지므로 런타임 예외만 롤백대상으로 삼는다.
하지만 원한다면 체크예외지만 롤백 대상으로 삼을 수 있다. rollbackFor또는 rollbackForClassName 속성을 이용해서 예외를 지정한다.
지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback한다. (-1일 경우 timeout을 사용하지 않는다)
트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용하거나 조회 기능만 감겨두어 조회 속도가 개선됨. insert, update, delete 작업이 진행되면 예외가 발생한다.