@Transactional에 대한 이해

이승주·2024년 7월 31일
3

개요

Spring에서 코드를 작성할 때 @Transaction을 자주 사용하곤했는데 DB에 무결성, 안전성을 보장해준다기에 사용하곤 했다. 하지만 어떤식으로 보장해주는지 잘 알지 못하는 것은 재대로 Spring을 사용하는 것이 아니기에 이번 기회에 정리해보려고한다.


선언적 트랜잭션 VS 프로그래밍적 트랜잭션

Spring에서는 Transaction 처리를 지원하는데 프로그래밍적 트랜잭션 처리와 선언적 트랜잭션을 지원해준다.

  1. 프로그래밍적 트랜잭션
  • Spring에서 제공하는 프로그래밍적 트랜잭션은 두 가지가 있다.
    -TransactionTemplate(프로그래밍적 트랜잭션 사용 시 Spring에서 권장 함)
    -PlatformTransactionManager
  1. 선언적 트랜잭션
  • 클래스, 메서드 위에 @Transactional을 추가해서 관리한다.

프로그래밍적 트랜잭션 =/= @Transactional

public class PointTransaction {

	private PointDao pointDao;
    private TransactionTemplate transactionTemplate;
    
    public void setPointDao(PointDao pointDao) {
    	this.pointDao = pointDao;
    }
    
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
    	this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
    
    public void invoke() {
    	dolnternalTransaction();
    }
    
    public void dolnternalTransaction() throws Exception {
    	transactionTemplate.execute(new TransactionCallbackWithoutResult() { 
        	protected void doInTransactionWithoutResult(TransactionStatus status) { 
            	try { 
                	pointDao.plusPoint() //<--------------------주목
                } catch (Exeption ex) { 
                	status.setRollbackOnly(); 
                } 
            } 
        });
    }
}

TransactionTemplate은 개발자가 Transaction을 시작, 종료 시점을 명시적으로 결정할 수 있도록 Spring에서 제공하는 하나의 방법이다.

위 코드에서는 pointPlus()메소드를 사용하기위해 Try-Catch과 transactionManager를 주입받고 Transaction 속성을 설정해주고 execute 메서드 내에서 로직을 실행한다.트랜잭션 설정으 하기위해 상당히 많은 코드를 볼 수 있다.


선언적 트랜잭션 = @Transactional

@Transactional
public void plusPoint() { //<--------------------주목
	pointRepository.updatePoint(10);
}

@Transactional 포함된 메서드가 호출될 경우 Spring은 해당 메서드에 대한 프록시(프록시패턴 디자인 패턴 중 하나)를 만드는데, PlatformTransactionManager를 사용해 트랜잭션을 시작하고, 정상 여부에 따라 커밋 또는 롤백을 알아서 해준다.

@Transactional 어노테이션을 plusPoint 메서드에 선언했다.
Spring이 plusPoint에 대한 프록시를 만들고 해당 로직이 잘 끝났으면 커밋 오류 나면 롤백을 시켜준다.

즉 프로그래밍적 트랜잭션에서는 번거롭게 Try-Catch문을 통해 Commit, RollBack을 해주어야했지만 Spring에서는 @Transactional 하나로 DB에 무결성, 안정성을 보장할 수 있다.


@Transactional annotation 지원 설정

  1. 트랜잭션의 전파 유형

  2. 트랜잭션의 격리 수준

  3. 트랜잭션에 의해 래핑 된 연산에 대한 시간제한

트랜잭션의 전파 유형

트랜잭션 전파(Transaction propagation)
어떤 트랜잭션이 동작중인 과정에서 다른 트랜잭션을 실행할 경우 어떻게 처리하는 가에 대한 개념이다.

1. Required

기본 전파 유형.
트랜잭션이 이미 있는 경우 현재 메서드가 해당 트랜잭션에 참여합니다.
트랜잭션이 존재하지 않으면 메서드에 대한 새 트랜잭션이 생성합니다.

REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample(String user) {
// ...
}

REQUIRED는 Default 이므로 생략이 가능합니다.

@Transactional
public void requiredExample(String user) {
// ...
}

2. Requires New

이 전파 유형은 항상 새 트랜잭션을 만듭니다
트랜잭션이 이미 존재하는 경우 새 트랜잭션이 완료될 때까지 일시 중단합니다.

Requires New

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample(String user) {
// ...
}

3. Nested

이 전파 유형은 중첩된 트랜잭션을 생성함.
트랜잭션이 이미 존재하는 경우 새 트랜잭션이 생성되어 기존 트랜잭션 내에 중첩됨.
트랜잭션이 존재하지 않으면 새 트랜잭션이 생성됨.

nested
@Transactional(propagation = Propagation.NESTED)
public void nestedExample(String user) {
// ...
}

4. Supports

이 전파 유형은 트랜잭션이 이미 존재하는 경우 트랜잭션을 지원.
트랜잭션이 존재하면 해당 트랜잭션 내에서 현재 메서드가 실행.
트랜잭션이 존재하지 않으면 트랜잭션 없이 메서드가 실행.

Supports
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) {
// ...
}

5. Mandatory

이 전파 유형은 트랜잭션이 이미 존재해야 합니다.
트랜잭션이 존재하지 않으면 예외가 발생합니다.

Mandatory
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) {
// ...
}

6. Not Supported

이 전파 유형은 트랜잭션을 지원하지 않음.
트랜잭션이 존재하면 현재 메서드가 실행되는 동안 트랜잭션이 일시 중단됨.

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample(String user) {
// ...
}

7. Never

이 전파 유형은 현재 메서드가 실행될 때 트랜잭션이 존재하지 않는지 확인한다.
트랜잭션이 존재하면 예외가 발생한다.

never
@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) {
// ...
}


트랜잭션의 격리 수준

  1. Read Uncommitted

  2. Read committed

  3. Repeatable Read

  4. Serializable

Spring Boot JPA 프로젝트에서 기본 격리 수준은 일반적으로 READ_COMMITTED임

Read Uncommitted

@Service
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public class UserService {
  
    @Autowired  
    private UserRepository userRepository;  
  
    public void updateUser(User user) {  
        userRepository.save(user);  
    }  
}

이 격리 수준을 가진 트랜잭션은 다른 concurrent 트랜잭션의 커밋되지 않은 데이터를 읽습니다.

또한 Nonrepeatable read와 Phantom read가 모두 발생할 수 있음
즉 행을 다시 읽거나 범위 쿼리를 다시 실행할 때 다른 결과를 얻을 수 있음


Read Committed

@Service
@Transactional(isolation = Isolation.READ_COMMITTED)
public class UserService {
  
    @Autowired  
    private UserRepository userRepository;  
  
    public User getUserById(Long id) {  
        return userRepository.findById(id)  
            .orElseThrow(() -> new EntityNotFoundException("User not found"));  
    }  
}

concurrent 트랜잭션에서 커밋되지 않은 변경 사항은 영향을 미치지 않지만, 트랜잭션이 변경 사항을 커밋하면 다시 쿼리 하여 결과가 변경될 수 있음


Repeatable Read

 

@Service
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class UserService {
  
    @Autowired  
    private UserRepository userRepository;  
  
    public User getUserById(Long id) {  
        return userRepository.findById(id)  
            .orElseThrow(() -> new EntityNotFoundException("User not found"));  
    }  
}

concurrent 트랜잭션에서 커밋되지 않은 변경 사항의 영향을 받지 않음
단)한 트랜잭션 안에서 일정 범위의 레코드를 두 번 이상 읽었을 때, 첫번째 쿼리에서 없던 레코드가 두번째 쿼리에서 나타남

트랜잭션 도중 새로운 레코드 삽입을 허용하기 때문에 나타나는 현상임


Serializable

@Service
@Transactional(isolation = Isolation.SERIALIZABLE)
public class UserService {
  
    @Autowired  
    private UserRepository userRepository;  
  
    public User getUserById(Long id) {  
        return userRepository.findById(id)  
            .orElseThrow(() -> new EntityNotFoundException("User not found"));  
    }  
}

앞서 언급한 모든 동시성 부작용을 방지하지만, concurrent 호출을 순차적으로 실행함
즉 트랜잭션 그룹을 정말로 serial 하게 실행하는 것과 동일한 결과를 가져옴


참고자료

https://itjava.tistory.com/33
https://colevelup.tistory.com/34
https://www.youtube.com/watch?v=taAp_u83MwA&t=219s

profile
반복되는 실수를 기록을 통해 줄여가보자!

0개의 댓글

관련 채용 정보