Spring에서 코드를 작성할 때 @Transaction을 자주 사용하곤했는데 DB에 무결성, 안전성을 보장해준다기에 사용하곤 했다. 하지만 어떤식으로 보장해주는지 잘 알지 못하는 것은 재대로 Spring을 사용하는 것이 아니기에 이번 기회에 정리해보려고한다.
Spring에서는 Transaction 처리를 지원하는데 프로그래밍적 트랜잭션 처리와 선언적 트랜잭션을 지원해준다.
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
public void plusPoint() { //<--------------------주목
pointRepository.updatePoint(10);
}
@Transactional 포함된 메서드가 호출될 경우 Spring은 해당 메서드에 대한 프록시(프록시패턴 디자인 패턴 중 하나)를 만드는데, PlatformTransactionManager를 사용해 트랜잭션을 시작하고, 정상 여부에 따라 커밋 또는 롤백을 알아서 해준다.
@Transactional 어노테이션을 plusPoint 메서드에 선언했다.
Spring이 plusPoint에 대한 프록시를 만들고 해당 로직이 잘 끝났으면 커밋 오류 나면 롤백을 시켜준다.
즉 프로그래밍적 트랜잭션에서는 번거롭게 Try-Catch문을 통해 Commit, RollBack을 해주어야했지만 Spring에서는 @Transactional 하나로 DB에 무결성, 안정성을 보장할 수 있다.
트랜잭션의 전파 유형
트랜잭션의 격리 수준
트랜잭션에 의해 래핑 된 연산에 대한 시간제한
트랜잭션 전파(Transaction propagation)
어떤 트랜잭션이 동작중인 과정에서 다른 트랜잭션을 실행할 경우 어떻게 처리하는 가에 대한 개념이다.
기본 전파 유형.
트랜잭션이 이미 있는 경우 현재 메서드가 해당 트랜잭션에 참여합니다.
트랜잭션이 존재하지 않으면 메서드에 대한 새 트랜잭션이 생성합니다.
REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample(String user) {
// ...
}
REQUIRED는 Default 이므로 생략이 가능합니다.
@Transactional
public void requiredExample(String user) {
// ...
}
이 전파 유형은 항상 새 트랜잭션을 만듭니다
트랜잭션이 이미 존재하는 경우 새 트랜잭션이 완료될 때까지 일시 중단합니다.
Requires New
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample(String user) {
// ...
}
이 전파 유형은 중첩된 트랜잭션을 생성함.
트랜잭션이 이미 존재하는 경우 새 트랜잭션이 생성되어 기존 트랜잭션 내에 중첩됨.
트랜잭션이 존재하지 않으면 새 트랜잭션이 생성됨.
nested
@Transactional(propagation = Propagation.NESTED)
public void nestedExample(String user) {
// ...
}
이 전파 유형은 트랜잭션이 이미 존재하는 경우 트랜잭션을 지원.
트랜잭션이 존재하면 해당 트랜잭션 내에서 현재 메서드가 실행.
트랜잭션이 존재하지 않으면 트랜잭션 없이 메서드가 실행.
Supports
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) {
// ...
}
이 전파 유형은 트랜잭션이 이미 존재해야 합니다.
트랜잭션이 존재하지 않으면 예외가 발생합니다.
Mandatory
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) {
// ...
}
이 전파 유형은 트랜잭션을 지원하지 않음.
트랜잭션이 존재하면 현재 메서드가 실행되는 동안 트랜잭션이 일시 중단됨.
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample(String user) {
// ...
}
이 전파 유형은 현재 메서드가 실행될 때 트랜잭션이 존재하지 않는지 확인한다.
트랜잭션이 존재하면 예외가 발생한다.
never
@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) {
// ...
}
Read Uncommitted
Read committed
Repeatable Read
Serializable
Spring Boot JPA 프로젝트에서 기본 격리 수준은 일반적으로 READ_COMMITTED임
@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가 모두 발생할 수 있음
즉 행을 다시 읽거나 범위 쿼리를 다시 실행할 때 다른 결과를 얻을 수 있음
@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 트랜잭션에서 커밋되지 않은 변경 사항은 영향을 미치지 않지만, 트랜잭션이 변경 사항을 커밋하면 다시 쿼리 하여 결과가 변경될 수 있음
@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 트랜잭션에서 커밋되지 않은 변경 사항의 영향을 받지 않음
단)한 트랜잭션 안에서 일정 범위의 레코드를 두 번 이상 읽었을 때, 첫번째 쿼리에서 없던 레코드가 두번째 쿼리에서 나타남
트랜잭션 도중 새로운 레코드 삽입을 허용하기 때문에 나타나는 현상임
@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