내배캠/TIL
[스프링] 트랜잭션 활용과 주의사항
모닝펄슨
2024. 10. 1. 23:50
ACID
- Atomicity, 원자성
- 트랜잭션 내 실행 작업들은 모두 하나의 작업처럼 성공하던가 실패해야 함. 만약 하나의 작업이 실패한다면 전체 rollback, 전체가 성공하면 commit 이 발생.
- Consistency, 일관성
- 트랜잭션의 수행 전후의 데이터가 데이터베이스의 규칙을 따르는지
- 트랜잭션 완료되면 데이터는 항상 db에서 정한 제약 조건 만족해야 함.
- Isolation, 격리성
- 동시에 실행되는 트랜잭션들이 서로에게 어떤 영향을 미칠 수 있는지 격리 수준 정하는 것.
- 트랜잭션과 동시성
- Durability, 지속성
- 트랜잭션이 성공적으로 끝나면 그 결과가 항상 기록되어야 한다는 의미
- 시스템에 문제가 발생하더라도 로그를 활용해 성공한 트랜잭션의 내용을 복구해야 함.
- WAL (Write Ahead Logging)
스프링에서 트랜잭션 관리 방법
롤백 정책 (rollbackFor)
- 기본 롤백 정책은 Runtime Exception
- 체크 예외는 기본적으로 롤백 발생하지 않음!!
- rollbackFor, noRollbackFor 를 사용해서 특정 예외를 rollback 정책 조건에 추가하거나 제거하고 싶을 경우 사용
/**
* 기본 rollback 정책
*/
@Transactional
public void updateCourseWithDefault() {
// ...
}
/**
* rollbackFor 활용
* 기존 rollback 정책 조건에 특정 예외를 추가시킨다.
*/
@Transactional(rollbackFor = Exception.class)
public void updateCourseWithRollbackFor() {
// ...
}
/**
* noRollbackFor 활용
* 기존 rollback 정책 조건에 특정 예외를 제거시킨다.
*/
@Transactional(noRollbackFor = RuntimeException.class)
public void updateCourseWithNoRollbackFor() {
// ...
}
전파 (Propagation)
트랜잭션이 메서드간 겹칠 때 어떻게 동작할지 설정.
- 기본 : Requried
- @Transactional 달려있는 메서드 호출할 때 그 메서드 안에서 또 @Transactional 달려있음.
- REEQUIRED
- 기본 전파 속성
- 있으면 참여, 없으면 생성
- REQUIRES_NEW
- 항상 새로운 트랜잭션 시작
- 기존 진행은 일단 보류
- 자식 트랜잭션이 커밋되면 이어서 진행
- SUPPORTS
- 기존 트랜잭션 있다면 참여
- 없으면 없이 진행
- NESTED
- 이미 진행중인 트랜잭션 있다면 중첩 트랜잭션 시작
- 부모 트랜잭션이 ROLLBACK되면 자신도 ROLLBACK
- DBMS마다 지원 여부 다름.
- NEVER
- 사용 불가
- 트랜잭션 존재하면 예외 발생
- MANDATORY, NOT_SUPPORTED …
트랜잭션 사용시 주의사항
수강 신청 시나리오를 가정했을 때, 로그에서 예외가 발생한다고 모든 프로세스가 실패하는 것은 바람직하지 않다. 그래서 로그 부분의 트랜잭션은 REQUIRES_NEW를 이용해 새로운 트랜잭션을 시작하도록 했다. 그러나 결과가 의도한 대로 나오지 않았다. 왜 그럴까?
@Transactional
public void processEnrollV1() {
printActiveTransaction("::: EnrollmentService.processEnrollV1()");
// 1. 수강 처리: Course 엔티티 생성
processCourse();
// 2. 결제 처리: Payment 엔티티 생성
processPayment();
// 3. 로그 처리: PaymentLog 로그 생성
try {
processLog();
} catch (Exception e) {
log.info("로그 예외 발생");
}
}
@Transactional
public void processCourse() {
printActiveTransaction("processCourse()");
// 1. 저장 - Course 저장
Course newCourse = Course.createNewCourse("newSpring");
courseRepository.save(newCourse);
}
@Transactional
public void processPayment() {
printActiveTransaction("processPayment()");
// 1. 저장 - Payment 저장
Payment newPayment = new Payment();
paymentRepository.save(newPayment);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processLog() {
printActiveTransaction("processLog()");
// 1. 저장 - PaymentLog 저장
PaymentLog newPaymentLog = new PaymentLog();
logRepository.save(newPaymentLog);
throw new RuntimeException("로그 예외 발생");
}
[결과]
EnrollmentService.processEnrollV1() - isActive: true, isNew: true
processCourse() - isActive: true, isNew: true
processPayment() - isActive: true, isNew: true
processLog() - isActive: true, isNew: true
최상단의 processEnrollV1를 제외한 나머지 메서드들에는 @Transactional 어노테이션이 적용되지 않는다!
트랜잭션 동작 원리
트랜잭션은 스프링에서 가장 대표적으로 사용하는 AOP 이다. 그리고 AOP 를 적용한 객체는 프록시 객체로 등록이 된다.
Self-invocation 문제
processCourse(), processPayment(), processLog() 는 Target(EnrollmentService) 본인 스스로가 본인의 메서드를 호출한 것이기 때문에 proxy 가 적용이 되지 않는다. 그렇기에 트랜잭션을 적용하고 싶다면 자기 자신의 메서드를 호출하는 것이 아니라, 따로 클래스를 만들어서 호출해줘야 한다.
+ REQUIRES_NEW의 오해
REQUIRES_NEW 라고 하면 완전히 분리된 전파 속성으로 받아드리는 경우가 많은데, 별도의 트랜잭션이라고 하더라도 하위 메서드에서 발생한 예외는 항상 상위 메서드로 전파되기 때문에 예외처리를 적절하게 해줘야 한다. 아니라면 REQUIRES_NEW로 새로운 트랜잭션을 생성했어도 자식에서 예외 발생하면 다 같이 롤백되는 수가 있다.