Spring Framework에서 제공하는 @Transactional 어노테이션 기능을 사용해서 트랜잭션을 관리하는 방법을 정리해 보았습니다.
@Transactional 어노테이션을 사용하면 데이터베이스와 관련된 작업을 안전하게 수행할 수 있습니다. 여기서 안전하다는 말은 데이터가 일관된 상태로 유지될 수 있다는 것을 의미합니다. 연관성이 있는 여러 개별 작업들을 트랜잭션이란 하나의 단위로 묶어서 처리하기 때문에 이러한 안전성을 보장할 수 있게 됩니다.
💡 트랜잭션이란?
- 여러 데이터베이스 작업을 하나의 단위로 묶어서 처리
- 작업 중간에 오류 발생시 모든 작업을 원상태로 되돌림
트랜잭션 프록시 생성
@Transactional 어노테이션이 적용된 메소드를 실행할 때, Spring은트랜잭션 프록시라는 객체를 생성합니다. 메소드가 호출되면 이 프록시 객체가 메소드의 전체 작업을 가로채서 트랜잭션 처리 로직을 추가합니다. 이로써 메소드가 안전히 트랜잭션으로서 시작되면, 트랜잭션 매니저가 호출되어 해당 메소드를 실행하고 제어합니다.
ACID 원칙을 지켜 작업 수행
여러 메소드를 호출해서 하나의 비즈니스 로직으로 수행하기를 원하는 경우에 '트랜잭션'으로서 관리해 줄 수 있습니다.
작업1와 결제 정보작업2를 모두 누락없이 데이터베이스에 저장해야 하는 경우작업1을 완료 후 사용자 정보의 이메일로 가입 완료 메일 전송하고, 해당 작업을 완료했다는 기록을 사용자 정보에 업데이트 해야 하는 작업작업2을 수행하는 경우작업1 수신자 계좌에 돈이 입금되는작업2 두 작업이 반드시 함께 성공하거나 실패해야 하는 송금 기능의 경우해당 클래스에 포함된 모든 메소드를 트랜잭션으로 처리합니다.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // 클래스 이름 위에 추가
public class AccountService {
// 여러 메소드들
}
특정 메소드만 트랜잭션으로 처리합니다.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
public void createOrder(Order order) {
// 주문 생성 로직
}
@Transactional // 메소드 이름 위에 추가
public void processOrder(Order order) {
// 주문 처리 로직
// 이 메소드에만 트랜잭션이 적용됨
}
public void cancelOrder(Long orderId) {
// 주문 취소 로직
}
}
트랜잭션 내에서 데이터를 변경하려고 시도하면 예외가 발생하도록 처리합니다.
@Service
public class OrderService {
@Transactional(readOnly = true) // 읽기 전용 적용
public Order getOrder(Long id) {
// 조회만 가능하고 수정은 불가
}
}
💡 격리 수준은 동시성 문제를 해결한다!
하나의 트랜잭션으로서 처리되는 메소드에 격리 수준이 설정된 경우, 해당 트랜잭션이 완전히 처리되기 전에는 다른 트랜잭션이 데이터를 읽을 수 없습니다. 아래의 격리 수준은 데이터를 읽을 수 있는 수준을 구체적으로 설정하는데 사용됩니다.
// 격리 수준 설정 예시
@Transactional(isolation = Isolation.SERIALIZABLE)
public void someDatabaseOperation() {
// 데이터베이스 작업
}
🟠 READ_UNCOMMITTED
커밋되지 않은 상태에서도 다른 트랜잭션이 변경 사항을 읽을 수 있습니다. Dirty Read 현상이 생길 수 있습니다.
🟠 READ_COMMITTED
다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다. 하지만 트랜잭션 중간에 데이터를 읽게 되면 Non-Repeatable 현상이 생길 수 있습니다.
🟠 REPEATABLE_READ
트랜잭션이 시작된 후 읽은 데이터는 트랜잭션이 끝날 때까지 변경되지 않습니다. (동일 트랜잭션 내에서 동일 데이터를 여러 번 읽어도 항상 결과가 같게 유지) 특정 데이터 항목(관계형 데이터베이스에서는 행)의 변경은 방지하지만, 새로운 데이터 항목의 추가나 기존 항목 삭제를 방지하지는 못하기 때문에 동일한 명령(쿼리)에서 Phantom Read 현상이 생길 수 있습니다.
🟠 SERIALIZABLE
가장 높은 격리 수준으로, 모든 트랜잭션이 순차적으로 실행되며 동시 실행이 불가능합니다.
@Transactional(isolation = Isolation.SERIALIZABLE)로 격리 수준을 설정하면 모든 트랜잭션이 순차적으로 동작하는 것처럼 실행하기 때문에 데이터 일관성을 보장할 수 있습니다.💡 격리 수준이 적합하지 않아 발생하는 현상
- Dirty Read: 수정되지 않은 데이터 값을 보는 현상
- Non Repeatable Read: 동일한 작업을 두 번 수행했을 때 두 결과가 다르게 나타나는 현상
- Phantom Read: 동일한 작업을 두 번 수행했을 때 두 결과의 전체 데이터 크기가 달라지는 현상
💡 전파 수준은 트랜잭션 경계를 관리한다!
전파 수준은 한 트랜잭션이 다른 트랜잭션 내에서 어떻게 처리되길 원하는지 지정합니다. 동시성 문제 해결을 위해 사용할 수도 있지만 격리 수준만큼 동시성 문제를 철저히 해결하지는 못합니다.
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED) // 트랜잭션 경계 지정 예시
public void processOrder(Order order) {
// 영향을 받는 로직
}
}
대표적으로 REQUIRED, REQUIRES_NEW, NESTED가 있습니다.
🟡 REQUIRED
기존 트랜잭션이 있으면 그 트랜잭션 내에 포함되어 실행되고, 없으면 스스로 새로운 트랜잭션을 시작합니다.
@Transactional이 사용되고, 프로세스B에 @Transactional(propagation = Propagation.REQUIRED) 사용🟡 REQUIRES_NEW
외부 메소드와 상관없이 항상 새로운 트랜잭션을 생성하고, 외부 메소드가 진행하던 기존 트랜잭션이 있는 경우 그 트랜잭션을 일시 정지시킵니다.
@Transactional(propagation = Propagation.REQUIRES_NEW) 사용🟡 NESTED
중첩된 트랜잭션을 생성합니다.
테스트 환경에서 @Transactional을 사용하면 Spring이 트랜잭션을 롤백하는 설정을 추가합니다. @Test, @SpringBootTest, @ExtendWith(MockitoExtension.class)같은 어노테이션이 붙은 테스트 환경에서는 테스트가 종료되면 트랜잭션이 자동으로 롤백되어 실제 데이터베이스가 테스트에서 사용된 데이터로 변경되지 않습니다.
@Test
@Transactional
void createDiary() {
diaryServiceImpl.createDiary(date, text);
verify(diaryRepository, times(1)).save(any(Diary.class));
// 테스트 종료 시 트랜잭션 롤백
}
참고
https://medium.com/@jkha7371/is-transactional-readonly-true-a-silver-bullet-1dbf130c97f8