Spring @Transactional
스프링에서는 Annotation 방식을 사용하는 선언적 트랜잭션을 지원합니다.
클래스, 메서드 위에 @Transactional
이 추가되면, 해당 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성되고, 이 프록시 객체는 해당 메소드가 호출될 경우 PlatformTransactionManager
를 사용하여 트랜잭션을 시작 및 정상 여부에 따라 Commit 또는 Rollback 합니다.
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionExcecption;
void commit(TransactionStatus status) throws TransactionExcecption;
void rollback(TransactionStatus status) throws TransactionExcecption;
}
단, private/protected 메서드는 @Transactional
을 무시하는데, 이는 Spring의 @Transactional
애노테이션이 스프링 AOP를 사용하여 구현되었고, 스프링 AOP가 다이나믹 프록시를 기반으로 동작하기 때문입니다.
AOP 동작 방식
Dynamic Proxy (Reflection)
CGLIB (Byte Code Instrument)
프록시 기반 AOP는 프록시 내에서 내부 함수를 호출할 때는 부가적인 서비스(여기서는 그게 바로 트랜잭션)가 적용되지 않습니다. 호출하려는 타겟을 감싸고 있는 프록시를 통해야만 부가적인 기능이 적용되는데, 이는 프록시 내에서 내부 함수를 호출 할 때는 감싸고 있는 영역을 거치지 않기 때문입니다.
(그러나 Spring boot는 기본적으로 proxy-target의 값이 true이므로 CGLib를 사용하여 프록시 객체를 생성합니다.)
이 @Transactional
로 트랜잭션을 구성할 경우, 아래와 같은 구성을 추가할 수 있습니다.
@Transactional
속성isolation
isolation 옵션은 트랜잭션에서 일관성없는 데이터의 허용 수준(격리 수준)을 정할수 있는 옵션입니다.
@Service
public class TransactionService {
@Transactional(isolation=Isolation.DEFAULT)
public void findSth(SomeDto someDto) throws Exception {
// do something
}
}
기본 설정으로, DBMS의 기본 Isolation Level을 적용합니다.
트랜잭션에 처리중인 혹은 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용합니다.
세 가지 동시성 부작용(Dirty Read, Nonrepeatable read, Phantom read)이 모두 발생할 수 있습니다.
Dirty Read | Nonrepeatable read | Phantom read |
---|---|---|
⭕️ | ⭕️ | ⭕️ |
트랜잭션이 커밋되어 확정된 데이터만을 읽는 것을 허용하는 방식입니다.
즉, 특정 사용자가 데이터를 변경하는 동안 다른 사용자는 해당 데이터에 접근이 불가합니다.
이렇게 하면 Dirty Read 문제는 방지할 수 있으나, Non-Repeatable Read 와 Phantom read 는 여전히 발생할 수 있습니다.
Postgres, SQL Server 및 Oracle의 기본 수준입니다.
Dirty Read | Nonrepeatable read | Phantom read |
---|---|---|
❌ | ⭕️ | ⭕️ |
트랜잭션이 완료될 때까지 SELECT 문장이 사용되는 모든 데이터들에 대해 Shared Lock이 걸려 다른 사용자가 그 데이터에 대한 접근이 불가능해집니다.
선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제하는 것을 불허함으로써 같은 데이터를 두 번 쿼리했을 때 일관성 있는 결과를 보장합니다.
Dirty Read | Nonrepeatable read | Phantom read |
---|---|---|
❌ | ❌ | ⭕️ |
해당 설정은 데이터의 일관성과 동시성을 유지하기 위해 MVCC 을 사용 하지 않습니다.
MVCC (Multi Version Concurrency Control)
다중 사용자 데이터 베이스 성능을 위한 기술로 데이터를 조회할때 LOCK을 사용하지 않고, 데이터의 버전을 관리하여 데이터 일관성과 동시성을 높이는 기술
그리고 트랜잭션이 완료될 때 까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock 을 걸어 다른 사용자가 해당 영역에 있는 모든 데이터에 대한 수정과 입력이 불가능하게 만들어 Phantom Read를 방지합니다.
Dirty Read | Nonrepeatable read | Phantom read |
---|---|---|
❌ | ❌ | ❌ |
설명만 보기에는 SERIALIZABLE 속성을 써야하겠지만, 격리수준이 높아질수록 성능저하의 우려가 있으니 상황에 따라 적절한 속성을 사용해야합니다.
propagation
propagation 옵션은 트랜잭션이 동작할 때 다른 트랜잭션이 호출되면(전파) 어떻게 처리할지를 정하는 옵션입니다.
즉 피호출 트랜잭션에서 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있습니다.
▶ REQUIRED
디폴트 속성으로, 이미 진행 중인 트랜잭션이 있으면 참여하고 진행 중인 트랜잭션이 없을 경우 새로운 트랜잭션을 생성합니다.
▶ SUPPORTS
이미 진행 중인 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 작업을 수행합니다.
▶ REQUIRES_NEW
항생 새로운 트랜잭션을 생성합니다. 이미 진행 중인 트랜잭션이 있다면 잠깐 보류하고 해당 트랜잭션 작업을 먼저 진행합니다.
이미 진행 중인 트랜잭션이 있어야만 작업을 수행합니다.
진행 중인 트랜잭션이 없다면 새로 시작하는 대신 예외를 발생시킵니다.
혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용합니다.
▶ NOT_SUPPORTED
이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행합니다.
▶ NEVER
트랜잭션을 사용하지 않도록 강제합니다.
트랜잭션이 있다면 Exception을 발생시킵니다.
▶ NESTED
이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션을 시작합니다.
중첩 트랜잭션 : 트랜잭션 안에 다시 트랜잭션을 만드는 것
하지만 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다르게, 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않습니다.
어떤 작업을 진행하는 중 로그는 꼭 DB에 저장해야 할 때, 이 로그를 저장하는 작업이 실패한다고 메인 작업의 트랜잭션까지는 롤백되버리면 문제가 됩니다.
반대로 로그를 남긴 후 메인 작업에서 예외가 발생한다면 이때는 저장한 로그도 롤백 되어야 하는게 맞습니다.
이럴 때 로그 작업을 메인 트랜잭션에서 분리해서 중첩 트랜잭션으로 만들어 두면 유용하게 사용할 수 있습니다.
noRollbackFor, rollbackFor
특정 예외 발생 시 rollback이 동작 혹은 동작하지 않도록 설정하는 속성입니다.
@Transactional은 Runtime Exception 이 아닌 Checked Exception일 경우 예외가 발생해도 자동적으로 롤백을 해주지 않습니다.
때문에 아래 코드의 경우, (3)에서 에러가 발생하더라도 (2)에서 적용된 업데이트 쿼리는 롤백되지 않습니다.
@Transactional
public void updateProfile(Long userId, UpdateProfileRequestDto profileRequest) {
// (1)
User user = getUserById(userId);
// (2)
user.updateProfile(profileRequest.getAccountName(), profileRequest.getNickname(), profileRequest.getIntroduce());
// (3)
if (!user.getAccountName().equals(profileRequest.getAccountName()) && isExistAccountName(profileRequest.getAccountName())) {
throw new BadRequestException(ResponseType.USER_DUPLICATED_ACCOUNT_NAME);
}
}
}
Checked Exception을 커밋 대상으로 삼은 이유는 스프링에서는 데이터 액세스 기술의 예외는 런타임 예외로 전환돼서 던져지기도 하고, Checked Exception은 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문입니다.
@Service
public class TransactionService {
@Transactional(rollbackFor=Exception.class)
public void addSth(SomeDto someDto) throws Exception {
// do something
}
}
timeout
지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback 하게 하는 옵션입니다. -1 이 default 값이고 이는 no timeout을 의미합니다.
@Service
public class TransactionService {
@Transactional(timeout=10)
public void addSth(SomeDto someDto) throws Exception {
// do something
}
}
readOnly
트랜잭션을 읽기전용으로 설정하는 속성으로, default 는 false 입니다. true로 설정하면 insert, update, delete 실행할 때 예외가 발생하여 write 하는 실수를 하더라도 데이터가 변경되는 것을 방지할 수 있습니다.
@Service
public class TransactionService {
@Transactional(readOnly = true)
public void findSth(SomeDto someDto) throws Exception {
// do something
}
}
트랜잭션에 readOnly = true 옵션을 주면 스프링 프레임워크가 하이버네이트 세션 플러시 모드를 MANUAL로 설정합니다.
이렇게 하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않기 때문에, 트랜잭션을 커밋하더라도 영속성 컨텍스트가 플러시 되지 않아 엔티티의 등록, 수정, 삭제가 동작하지 않습니다.
또한 읽기 전용으로 영속성 컨텍스트는 변경 감지를 위한 스냅샷을 보관하지 않으므로 성능이 향상됩니다.
즉 엔티티를 읽기 전용으로 조회하면 변경 감지를 위한 스냅샷을 유지하지 않아도 되고, 영속성 컨텍스트를 플러시 하지 않아도 되므로 성능을 최적화할 수 있습니다.
Ref.
Spring의 @Transactional과 AOP 그리고 CGLib와 Dynamic Proxy(JDK Proxy)
6.4.3. JDK- and CGLIB-based proxies
Spring Transaction 관리에 대한 메모
[Spring] @Transactional 사용시 주의해야할 점
@Transactional 동작 원리
Spring에서 AOP를 구현하는 방법과 Transactional
Transaction Propagation and Isolation in Spring @Transactional
[Spring 3 - Transaction] 트랜잭션 추상화 클래스의 종류와 사용법