트랜잭션이란?
트랜잭션은 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위. 하나의 트랜잭션은 여러 개의 연산을 포함.
이때, 모두 성공적으로 완료됨 (Commit), 아니면 전혀 실행되지 않아야 함 (Rollback) - 데이터 일관성, 무결성을 유지하기 위해서
선언적인 트랜잭션 관리
선언적인 트랜잭션 관리란?
트랜잭션을 처리하는 코드를 직접 작성하지 않고, 어노테이션이나 XML을 통해 경계를 설정하는 것
이로써, 개발자는 트랜잭션 관리 로직에 신경 쓸 필요 없이 비지니스 로직에 집중할 수 있다.
@Transactional
@Target({ElementType.TYPE, ElementType.METHOD})
으로 선언되어 있어 클래스, 메서드 영역에 작성한다.@Retention(RetentionPolicy.RUNTIME)
이니 런타임까지 유지가 됩니다.
private 접근 제한자 일 때는 작동하지 않는다.
-> 이는 Spring AOP를 사용하기 때문에 프락시 객체는 private를 호출할 수 없기 때문이다.
메서드 단위나 클래스 단위에 어노테이션을 이용해 제어를 한다.
// 클래스 전역적으로 선언된다.
@Transactional
public class TrancationService {
public void run() {}
}
public class TrancationService {
@Transactional
public void run() {
// 아래쪽 영역이 작동이 된다.
}
}
@Transactional의 속성들
1. readOnly
이 설정은 주로 데이터베이스 성능 최적화를 목적으로 사용됩니다. 내부적으로 readOnly
가 true
로 설정되면,
최적화를 수행합니다.
- readOnly=false
@Transactional(readOnly = false)
public void readOnlySave() {
Optional<Sample> optionalSample = sampleRepository.findById(1L);
Sample sample = optionalSample.get();
sample.setText("Hello World!2");
sampleRepository.save(sample);
}
Flush session이 발생 하면서 플러시를 유발한다.
- readOnly=true
@Transactional(readOnly = true)
public void readOnlySave() {
Optional<Sample> optionalSample = sampleRepository.findById(1L);
Sample sample = optionalSample.get();
sample.setText("Hello World!2");
sampleRepository.save(sample);
}
Flush가 발생하지 않는다.
- readOnly=true , 더티 체킹
@Transactional(readOnly = true)
public void readOnlySave() {
Optional<Sample> optionalSample = sampleRepository.findById(1L);
Sample sample = optionalSample.get();
sample.setText("Hello World!2");
}
데이터가 변경되지 않는다
- readOnly=true, saveAndFlush 수행
@Transactional(readOnly = true)
public void readOnlySave() {
Optional<Sample> optionalSample = sampleRepository.findById(1L);
Sample sample = optionalSample.get();
sample.setText("Hello World!2");
sampleRepository.saveAndFlush(sample);
}
flush가 발생하여 데이터가 변경이 된다.
2. timeout
타임아웃은 트랜잭션의 시작부터 끝까지 걸린 시간을 의미하며 기본값은 -1인 (무제한)으로 되어 있다.
isolation - 격리 단계
- READ_UNCOMMITTED : 다른 트랜잭션에 커밋되지 않은 변경 내용을 트랜잭션이 볼 수 있음
- READ_COMMITTED : 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다.
- REPEATABLE_READ : 트랜잭션이 시작될 때와 읽을 때까지 동일하게 유지가 됨
- Mysql의 기본 전략
- 하이버네이트인 경우에는 영속성 콘텍스트를 사용하면 애플리케이션에서의 격리단계를 보장할 수 있다.
- SERIALIZABLE : 트랜잭션이 완전히 순차적으로 실행됨
3. propagation - 전파 속성
- REQUIRED (기본값)
- 활성 트랜잭션이 있으면 트랜잭션을 재사용함 없으면 새로운 트랜잭션을 염
- SUPPORTS
- 현재 활성 트랜잭션이 있으면 이를 사용합니다. 그렇지 않으면 트랜잭션 없이 실행합니다.
- MANDATORY
- 반드시 현재 활성 트랜잭션이 있어야 하며 아닐 시 예외를 발생함
- REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작합니다. 현재 활성 트랜잭션이 있으면, 그 트랜잭션을 일시 중단시킵니다.
- 데드락 발생 여지가 있음
- NOT_SUPPORTED
- 트랜잭션 없이 실행함
- NAVER: 트랜잭션 없이 실행합니다. 현재 활성 트랜잭션이 있으면 예외를 발생시킵니다.
- NESTED: 현재 트랜잭션 내에 중첩된 트랜잭션을 시작합니다. 중첩된 트랜잭션은 외부 트랜잭션과는 독립적으로 커밋 또는 롤백될 수 있습니다. 활성 트랜잭션이 없으면, 이 옵션은
REQUIRED
와 동일하게 동작합니다.
REQUIRES_NEW의 호출자 트랜잭션의 연관성?
결론부터 말하자면 호출되는 이의 예외에 대해서 롤백이 된다. -> 트랜잭션은 새로 열린다.
@Transactional(propagation = Propagation.REQUIRED)
public void readOnlySave() {
Sample sample = new Sample();
sample.setText("Hello World!2");
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("readOnlySave Transaction {} ", currentTransactionName);
log.info("readOnlySave Transaction active {} ", TransactionSynchronizationManager.isActualTransactionActive());
sampleRepository.save(sample);
sampleService2.error();
}
// sampleService2 -
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void error() {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("error Transaction {} ", currentTransactionName);
log.info("error Transaction active {} ", TransactionSynchronizationManager.isActualTransactionActive());
throw new RuntimeException("SampleService");
}
결론
트랜잭션의 readOnly 속성을 true로 주어도 flush가 발생하면 데이터가 반영됨을 알 수가 있다.
REQUIRES_NEW의 호출자를 사용해도 예외가 발생한다면 부모 메서드도 롤백이 됨을 알 수 있다.