Transaction 어노테이션을 정리 하기 위해 작성한 포스트.
참고: https://mangkyu.tistory.com/154
Transaction 동기화, 추상화 처리를 위해서는 직접 코드를 작성 해야 했지만,
Spring에서는 마치 트랜잭션 코드와 같은 코드가 존재하지 않는 것 처럼 보이기 위해
해당 로직을 클래스 밖으로 빼내서 별도의 모듈로 만드는
AOP를 고안 및 적용하게 되었고, 이를 적용한 어노테이션(@Transactional)을 지원하게 되었다.
트랜잭션 속성들은 각각의 트랜잭션 경계설정 속성에 지정할 수 있다.
하지만 보통은 readOnly 속성 정도만 활용하고, 나머지는 디폴트로 사용하는 경우가 많다.
세밀한 속성은 DB나 WAS의 트랜잭션 매니저 설정을 이용해도 되기 때문이다.
Spring이 제공하는 선언적 트랜잭션(@Transactional 어노테이션)의 장점 중 하나는 여러 트랜잭션 적용 범위를 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점이다.
Spring이 트랜잭션을 어떻게 진행시킬지 전파 속성을 선언해야 하는데, 이를 통해 새로운 트랜잭션을 시작할지 또는 기존의 트랜잭션에 참여할지를 결정하게 된다.
Spring이 지원하는 7가지 전파 속성.
REQUIRED
SUPPORTS
MANDATORY
REQUIRES_NEW
NOT_SUPPORTED
NEVER
NESTED
REQUIRED
REQUIRED는 Defalut 값이다.
REQUIRED는 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
만약 REQUIRED 속성일 때 하나의 트랜잭션이 시작된 후 다른 트랜잭션 경계가 설정된 메소드를 호출하면 자연스럽게 같은 트랜잭션으로 묶인다.
SUPPORTS
SUPPORTS는 이미 시작 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행한다.
트랜잭션이 없기는 하지만 해당 경계 안에서 Connection 객체나 하이버네이트의 Session 등은 공유를 할 수 있다.
MANDATORY
MANDATORY 역시 이미 시작된 트랜잭션이 있으면 참여한다.
하지만 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다.
즉, MANDATORY는 혼자서 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용할 수 있다.
REQUIRES_NEW
REQUIRES_NEW는 항상 새로운 트랜잭션을 시작해야 할 때 사용한다.
만약 이미 시작된 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.
만약 JTA 트랜잭션 매니저를 사용한다면 서버의 트랜잭션 매니저에 트랜잭션 보류가 가능하도록 설정되어 있어야 한다.
NOT_SUPPORTED
NOT_SUPPORTED는 이미 진행중인 트랜잭션이 있으면 이를 보류시키고, 트랜잭션을 사용하지 않도록 한다.
NEVER
NEVER는 이미 진행중인 트랜잭션이 있으면 예외를 발생시키며, 트랜잭션을 사용하지 않도록 강제한다.
트랜잭션 격리수준은 동시에 여러 트랜잭션이 진행될 때 트랜잭션의 작업 결과를 여타 트랜잭션에게 어떻게 노출할 것인지를 결정한다. 스프링은 다음의 5가지 격리수준 속성을 지원한다.
DEFAULT
DEFAULT는 사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다.
(일반적으로 드라이버의 격리 수준 = DB의 격리 수준)
대부분의 DB는 READ_COMMITED를 기본 격리수준으로 갖는다.
일부 DB는 디폴트 값이 다른 경우도 있으므로 DEFAULT를 사용할 경우 문서를 참고해서 기본 격리수준을 확인해야 한다.
READ_UNCOMMITTED
READ_UNCOMMITTED는 가장 낮은 격리수준으로써 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있다.
하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용한다.
READ_COMMITTED
READ_COMMITTED는 가장 많이 사용되는 격리수준이다.
Spring의 DEFAULT값으로 지정되어 있다( DB는 일반적으로 READ_COMMITED로 되어있기 때문)
READ_COMMITTED는 READ_UNCOMMITTED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다.
대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다.
이 때문에 처음 트랜잭션이 같은 로우를 다시 읽을 때 다른 내용이 발견될 수 있다.
REPEATABLE_READ
REPEATABLE_READ는 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없도록 막아준다.
하지만 새로운 로우를 추가하는 것은 막지 않는다.
따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.
SERIALIZABLE
SERIALIZABLE은 가장 강력한 트랜잭션 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시켜준다.
그렇기 때문에 SERIALIZABLE은 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스할 수 없다.
SERIALIZABLE은 가장 안전하지만 가장 성능이 떨어지는 격리수준이기 때문에 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용해서는 안된다.
@Transactional 어노테이션의 isolation 엘리먼트로 원하는 전파 속성을 지정할 수 있으며, 기본은 DEFAULT로 설정되어 있다.
Spring에서는 트랜잭션을 2가지 목적(성능 최적화, 쓰기 방지)으로 읽기 전용(readOnly)으로 설정할 수 있다.
읽기 전용으로 설정이 되어 있으면 트랜잭션을 준비하면서 트랜잭션 매니저에게 이러한 정보가 전달되며 그에 따라 트랜잭션 매니저가 적절한 작업을 수행한다.
그런데 일부 트랜잭션 매니저의 경우 읽기전용 속성을 무시하고 쓰기 작업을 하용할 수도 있으므로 주의해야 한다.
물론 일반적으로 읽기전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE의 작업이 진행되면 예외가 발생한다.
timeout 속성을 이용하면 트랜잭션에 제한시간을 지정할 수 있다.
값은 int 타입의 초 단위로 지정할 수 있는데 문자열로 지정하기를 원한다면 timeoutString을 사용하면 된다.
만약 별도로 값을 설정해주지 않는다면 트랜잭션 시스템의 제한시간을 따르며, 제한 시간을 직접 지정했는데 트랜잭션 매니저에서 이 기능을 지원하지 못한다면 예외가 발생할 수 있다.
선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백하고, 예외가 발생하지 않았거나 체크 예외가 발생하였다면 커밋한다.
여기서 체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기 보다는 반환값을 대신해 비지니스적인 의미를 담은 결과로 많이 사용되기 때문이다.
스프링에서는 데이터 액세스 기술의 예외는 런타임 예외로 전환하여 던지므로 런타임 예외만 롤백 대상으로 삼은 것이다.
하지만 롤백/커밋의 동작 방식의 변경을 원한다면 설정을 통해 동작 방식을 바꿀 수 있는데, 커밋 대상이지만 롤백을 발생시킬 예외나 클래스 이름은 각각 rollbackFor 또는 rollbackForClassName으로 지정할 수 있으며, 반대로 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정하기 위해서는 noRollbackFor 또는 noRollbackForClassName을 이용할 수 있다.
어노테이션은 다음과 같이 사용한다
비지니스 로직에 사용 : DB에서 읽어온 데이터를 사용하고 변경하는 등의 작업을 하는 곳은 일반적으로 서비스 계층이기 때문
읽기 전용 트랜잭션의 공통화 : 클래스 레벨에 공통적으로 ReadOnly 옵션 트랜잭션 어노테이션을 선언하고, 추가나 삭제 또는 수정이 있는 작업에는 쓰기가 가능하도록 별도의 @Transacional 어노테이션을 메소드에 선언하는 것이 좋다.
테스트 코드에서의 사용 : 트랜잭션 어노테이션을 테스트 코드에 붙이면 테스트의 DB 커밋을 롤백해주는 기능이 있다.