개발

[ Spring ] @Transactional 정리

beng9re 2024. 3. 31. 16:30

트랜잭션이란?


트랜잭션은 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위. 하나의 트랜잭션은 여러 개의 연산을 포함.

이때, 모두 성공적으로 완료됨 (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

이 설정은 주로 데이터베이스 성능 최적화를 목적으로 사용됩니다. 내부적으로 readOnlytrue로 설정되면,

최적화를 수행합니다. 

  • 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의 호출자를 사용해도 예외가 발생한다면 부모 메서드도 롤백이 됨을 알 수 있다.