@Transactional 알짜만 빼먹기! with JPA
@Transactional 이란
@Transactional 은 Spring에서 제공하는 트랜잭션 관리 기능을 적용할 때 사용되며, 특정 메서드를 하나의 트랜잭션 단위로 지정할때 사용하는 어노테이션 입니다.
일반적으로 하나의 메서드에서 복수의 데이터 처리를 할 때 붙여서 사용합니다. 예외가 발생하면 롤백을 해서 데이터 정합성을 유지해야 하기 때문이죠.
이번 포스팅에서는 @Transactional을 사용하는데 꼭 알아야 하는 알짜만 빼먹는 시간을 갖겠습니다.
Checked, Unchecked Exception
@Transactional을 사용했다는 것은 어떤 예외가 발생했을 때 Rollback 처리를 하기 위해서겠죠?
그럼 어떤 Exception이 발생했을 때 Rollback 처리가 되는지를 알아야 합니다.
결론부터 말씀드리면 @Transactional은 Unchecked Exception 이 발생했을 때만 모든 작업을 취소하고 Rollback을 수행하게됩니다.
Unchecked Exception
Unchecked Exception은 RuntimeExpcetion을 상속받은 모든 Exception을 말합니다. 일반적으로 컴파일 시점에 체크 할 수 없는 Exception들을 말합니다. NullpointerException, ClassCastException 등이 있습니다.
Java 11 기준 RuntimeException을 상속받는 Exception은 아래의 Link를 참고하세요.
Link : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/RuntimeException.html
Checked Exception
컴파일 시점에 체크할 수 있는 Exception들을 말합니다. 개발자가 해당 예외를 처리하도록 강제하기 때문에 try-catch 를 사용하거나 메서드에 throws를 추가해야만 합니다.
@Transactional을 사용해도 예외는 발생하지만 트랜잭션 커밋은 발생하게 되니 주의해야 합니다.
IOException, SQLException 등이 있습니다.
NestedRuntimeException, NestedCheckedException
위 Exception 은 Spring Framework에서 중첩된 예외 처리를 위한 예외 클래스로 사용되며, Spring 애플리케이션에서 예외 처리와 오류 로깅에 유용합니다. 둘의 차이는 이름에서도 알수 있듯이 Checked이냐 UnChecked 이냐의 차이 입니다.
javaX 의 Transactional VS springframework 의 Transactional
IDE에서 자동으로 @Transactional을 추가하다보면 두개의 어노테이션이 있는 것을 확인하실 수 있을겁니다.
javax.transaction.Transactional 과 org.springframework.transaction.annotation.Transactional 이 그 둘입니다.
이 두개의 어노테이션은 다른 패키지에 있을 뿐 같은 동작을 합니다. 하지만 Spring을 활용하고 있다면 더 많은 기능을 옵션적으로 제공하는 Spring의 annotation을 사용하도록 합니다.
javax.transaction.Transactional
- JavaEE 환경에서 사용.
- JTA(Java Transaction API)와 함께 동작
org.springframework.transaction.annotation.Transactional
- Spring Framework 환경에서 사용.
- Spring 트랜잭션 관리 기능을 사용하여 트랜잭션 처리.
- Spring 의 다양한 기능과 통합 가능하며, Spring Boot 등과 함께 사용하기 용이.
사용방법
사용방법은 동일하지만, org.springframework.transaction.annotation.Transactional 기준으로 설명하겠습니다.
트랜잭션 범위 내 에서 처리하기 위한 메서드 상단에 추가하여 사용합니다. 간단하죠!
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TransactionTestService {
private final TestRepository repository;
@Transactional()
public void insertTest() {
/*insert code*/
}
}
옵션
Isolation
트랜잭션의 격리 수준 옵션 입니다. 동시에 여러 사용자가 데이터 접근할 때 어디까지 하용할지를 설정하는 옵션입니다.
격리 수준이 약할수록 데이터 접근 및 수정이 자유롭지만 일관성이 떨어지고 격리 수준이 강해진다면 데이터의 일관성이 증가합니다.
4가지 옵션이 존재합니다. 기본 값은 Isolation.DEFAULT.
Isolation.DEFAULT | 데이터베이스의 기본 격리 수준으로 설정합니다. |
Isolation.READ_UNCOMMITED | Dirty Read, Non-Repeatable Read, Phantom Read 가 발생 가능합니다. |
Isolation.READ_COMMITED | Non-Repeatable Read, Phantom Read 가 발생 가능 합니다. |
Isolation.REPEATABLE_READ | Phantom Read 가 발생 가능 합니다. |
Isolation.SERIALIZABLE | 가장 엄격한 격리 수준으로 설정합니다. |
Dirty Read - 커밋되지 않은 데이터를 다른 트랜잭션에서 접근 가능.
Non-Repeatable Read - 아직 커밋 전에 변경된 데이터를 다른 트랜잭션에서 접근 가능.
Phantom Read - 하나의 트랜재션은 하나의 스냅샷만 사용. 처음 조회한 데이터만 조회됨.
Propagation
부모 레벨의 트랜잭션이 존재할 때 새로운 트랜잭션이 어떤 정책을 사용할지에 대한 정의입니다. 기존 트랜잭션을 그대로 사용하거나, 새로운 트랜잭션을 생성하거나, 트랜잭션을 사용하지 않은 상태(non-transactional : commit, rollback 안됨)로 실행할 수있습니다. 기본 값은 Propagation.REQUIRED 입니다.
REQUIRED | 부모 레벨 트랜잭션이 있을 경우 사용. 없는 경우 새로운 트랜잭션 시작 |
SUPPORTS | 부모 레벨 트랜잭션이 있을 경우에만 사용 |
MANDANTORY | 부모 레벨 트랜잭션이 없으면 예외 발생 |
REQUIRES_NEW | 무조건 새로운 트랜잭션 시작 |
NOT_SUPPORTED | 부모 레벨 트랜잭션 일시 중지, non-transactional 상태로 샐행 |
NEVER | non-transactional 상태로 실행. 부모 레벨 트랜잭션이 있으면 예외 발생 |
NESTED | 트랜잭션이 없을 때만 실행, 그 외에는 예외 발생 |
readOnly
트랜잭션을 읽기 전용으로 설정하는 옵션입니다. 기본 값은 false.
'true'로 설정할 경우 읽기 작업만 허용되고 쓰기 작업 시 예외가 발생합니다.
rollbackFor
지정된 예외 클래스 배열에 해당하는 예외가 발생했을 경우만 트랜잭션을 롤백합니다.
기본 값은 RuntimeException을 상속받은 Exception (Unchecked Exception).
timeout
트랜잭션 타임아웃 시간을 지정합니다.
기본값은 -1로 데이터베이스의 타임아웃 설정을 따릅니다.
초단위 설정.
옵션 사용 예시
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TransactionTestService {
private final TestRepository repository;
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED,
readOnly = true, rollbackFor = {CustomException.class}, timeout = 30)
public void insertTest() {
/*insert code*/
}
}
@Transactional의 동작 원리
프록시 생성
@Transactional 이 사용된 메서드가 호출되면 Spring은 AOP(Aspect-Oriented Programming) 프록시를 생성합니다. 이 프록시는 원본 메서드 호출 전, 후에 트랜잭션 관련 동작을 적용하기 위한 로직을 가지고 있습니다.
트랜잭션 시작
프록시는 원본 메서드 호출 전에 트랜잭션을 시작합니다. 이 때, 트랜잭션의 격리수준(Isolation) 및 전파 동작(Propagation) 등은 어노테이션 설정에 따라 결정됩니다.
메서드 실행
네서드 내에서 발생하는 데이터베이스 관련 작업은 트랜잭션 내에서 실행됩니다.
트랜잭션 커밋 또는 롤백
메서드 실행이 완료되면 프록시는 트랜잭션 성공 또는 실패 여부를 확인하여 성공하면 커밋, 실패하면 트랜잭션을 롤백합니다.
트랜잭션 종료
종료된 트랜잭션이 데이터베이스에 반영됩니다.
JPA SimpleJpaRepository의 save method
SimpleJpaRepository 를 보시면 클래스 레벨에 '@Transactional(readOnly = true)' 가 있는 것을 보실 수 있습니다. 이는 해당 레포지토리의 메서드 대부분이 읽기 전용으로 동작한다는 의미 입니다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
}
그럼 SimpleJpaRepository 의 save 메서드는 왜 오류를 발생 시키지 않고 동작할까요?
save 메서드를 보시죠.
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
save 메서드에는 @Transactional 이 추가되어 있습니다. @Transactional이 없다면 데이터를 저장하거나 수정하는 save 메서드 호출 시 에러가 발생하게 될 것입니다. (@Transactional(readOnly = true) 옵션에 따라)
SimpleJpaRepository의 다른 메서드들은 '@Transactional(readOnly = true)' 옵션으로 동작, @Transactional 이 추가된 메서드들은 readOnly = false 로 동작한다는 것을 알 수 있습니다.
self-invocation
spring framework에서 트랜잭션을 관리할 때 발생하는 하나의 상황을 지칭하는 용어입니다. 이는 스프링의 AOP 프록시 메커니즘과 관련이 있습니다.
스프링의 트랜잭션 관리 방식은 프록시 패턴을 사용하여 구현되는데, 이 프록시는 원본 메서드 호출 전후에 추가적인 동작을 수행하는 역할을 합니다. 그렇기 때문에 트랜잭션을 적용한 메서드 내에서 다른 메서드를 호출하는 경우, 이 내부 호출을 self-invocation이라고 합니다.
원본 메서드 내부에서 호출되는 메서드에서 발생한 예외가 원본 메서드까지 전달되지 않는 경우 롤백이 동작하지 않으니 throw, throws를 통해서 원본 메서드까지 전달되도록 해야 합니다.