@Transactional 은 어떻게 동작하는가 ?
들어가기 전에
SpringBoot 를 사용하여 데이터 베이스를 연결할 때, 사용하던
@Transactional 어노테이션이 있다. 해당 어노테이션이 어떻게 동작하는지
Spring AOP 를 사용해서 작동하는 것으로 간략히 알고 있다.
그래서 이번에 조금 제대로 알아 보려고 한다.
@Transactional
Spring Framework에서 하나의 작업 단위를 트랜잭션으로 묶어주는 어노테이션이다.
데이터베이스의 일관성과 정합성을 보장하기 위해 사용한다.
예를 들어, 주문
과 상품
이라는 두 개의 테이블이 있다고 가정하자
사용자가 어떤 상품을 주문하면 다음과 같은 작업이 동시에 일어나야 한다.
- 주문 테이블에 주문 정보가 저장된다.
- 상품 테이블의 재고 수량이 주문 수량만큼 감소한다.
이때, 두 작업은 동시에 반드시 성공하거나 동시에 실패 해야한다.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order, Product product, int quantity) {
orderRepository.save(order);
product.reduceQuantity(quantity);
productRepository.save(product);
}
}
기능 1. 트랜잭션 커밋과 롤백
트랜잭션이 시작되며, 정상적으로 수행이되면 Commit
, 예외가 발생하면 rollback
이 된다.
기능 2. 자동 롤백
기본적으로 RuntimeException, Error 가 발생하면 Rollback 이 된다.rollbackFor
로 롤백이 되는 예외 클래스를 설정할 수 있다.
단, Checked Exception ( 컴파일러가 예외 처리를 강제하는 예외 ) 은 rollback 되지 않는다.
@Transactional
public void saveData() throws SQLException {
saveToDB(); // 이 메서드에서 SQLException( Checked Exception ) 발생 → 기본적으로 rollback 안 됨!
}
@Transactional
public void saveData2() throws Exception {
saveToDB(); // 이 메서드에서 Exception(Checked) 발생 → rollback 됨
}
기능 3. 전파 속성
트랜잭션이 이미 존재할 때, 새로운 트랜잭션을 시작할지 기존 트랜잭션에
참여 할지를 정의한다.
REQUIRED ( 기본값 ) : 트랜잭션이 있으면 참여하고 없으면 새로 생성한다.
REQUIRES_NEW : 무조건 새로운 트랜잭션 시작하고, 기존 트랜잭션은 일시 정지
NESTED : 현재 트랜잭션안에 중첩으로 트랜잭션 실행
여기서 트랜잭션이 없으면 참여하고 있으면 생성하는 부분에 대해 이해 하려면트랜잭션은 ThreadLocal 에 저장
되어 있어, 같은 쓰레드 내에서 공유
가 된다.
트랜잭션이 이미 존재한다는 말은 현재 쓰레드에서 트랜잭션이 이미 시작된 상태를 의미한다.
// REQUIRED
@Transactional // 트랜잭션이 이미 있으면 참여
public void serviceA() {
serviceB(); // 여기서도 @Transactional이면 같은 트랜잭션에 참여
}
// REQUIRES_NEW 예시
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logError() {
// 별도의 트랜잭션으로 실행되어, serviceA가 실패해도 이건 커밋됨
}
여기서 serviceA
가 logError
를 호출하여 실행했을때, serviceA 가 예외가 나더라도
logError 는 rollback 되지 않고 된다
기능 4. 격리 수준 설정
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processAccount() {
// 계좌 이체, 잔액 확인 등 높은 정합성이 필요한 작업
}
동시성 문제를 해결하기 위해 트랜잭션 격리 수준을 설정할 수 있다.
READ_UNCOMMITTED : Commit 되지 않는 Record 를 읽음
READ_COMMITTED ( 기본값 ) : Commit 된 Record 만 읽음
REPEATABLE_READ : 같은 트랜잭션 내에서는 데이터를 반복해서 읽을 수 있음
SERIALIZABLE : 트랜잭션을 순차적으로 실행하는 것 처럼 동작, 다른 트랜잭션이 동시에 같은 데이터를 읽거나 쓸 수 없음
@Transactional 는 어떻게 동작하는가 ?
위는 @Transaction 이 어떤 기능을 가지는지 간략하게 알아보았는데, 이제 @Transactional 이
어떻게 동작하는지 알아보자
흔히 @Transactional 이 작동하는 방식은 AOP 를 사용하여 동작한다고 한다.
AOP 는 Aspect Oriented Programming 으로 관점 지향 프로그래밍이라고 하며,
공통적으로 반복되는 코드를 비지니스 로직과 분리하고 필요시점에서 실행 하는 패러다임을 말한다.
왜 관점 지향 이라는 말이 나오는 것일까?
흔히 OOP 는 관심사를 객체로 나누는 것을 말한다.
나눠진 여러 관심사들 중에서 공통되는 부가 기능들이 있는데, 이를 관점이라고 부른다.
@Transactional 동작 순서
1. @Transactional 메서드가 실행되고 Proxy 가 호출을 가로챈다.
AOP 기반으로 동작하며, 프록시 객체가 메서드 호출을 가로챈다.
2. @Transactional 의 Propagation 정책에 따라 트랜잭션 시작
REQUIRED, REQUIRED_NEW, NESTED 등의 전파 정책에 따라 트랜잭션이 시작된다.
3. Connection Pool 에서 DB Connection 을 획득
Connection Pool 정책에 따라 DB Connection 을 획득한다.
4. 비지니스 로직 수행
@Transaction 이 선언된 메서드 내용을 수행한다.
5. 예외가 발생했는지 확인하고, 트랜잭션 종료 처리를 한다.
예외가 발생하지 않으면 Commit, 예외가 발생하면 정책에 따라 Rollback 처리한다.
6. Connection 을 반환
Connection 을 Connection Pool 로 반환한다.
@Transactional 사용 시 주의해야할 점
1. 프록시 방식(AOP) 기반이라서 **내부 메서드 호출은 트랜잭션이 적용되지 않는다.**
스프링 AOP는 프록시 패턴을 사용하기 때문에, 외부에서 호출될 때만 트랜잭션 기능이 적용되기 때문에
같은 클래스 내에서 @Transactional 메서드를 호출하면, 트랜잭션이 적용되지 않는다.
@Service
public class MyService {
@Transactional
public void methodA() {
// 트랜잭션 시작됨
methodB();
}
@Transactional
public void methodB() {
}
}
2. public 메서드에만 적용된다
기본적으로 스프링 AOP는 public 메서드만 프록시를 통해 트랜잭션을 적용
3. 메서드가 끝나기전에 Transaction 종료하면 오작동의 위험이있다.
트랜잭션을 보장하려면, 반드시 트랜잭션 범위 안에서 모든 DB 작업을 수행해야 한다.
트랜잭션이 끝나기 전에 `Connection`을 강제로 닫거나, flush를 호출하면 동작이 꼬일 수 있다.
4. 트랜잭션의 범위를 명확히
트랜잭션 범위가 너무 크면, 커넥션 점유 시간과 락 대기 시간이 길어져
성능 저하나 데드락의 원인이 된다
글을 마치며
@Transactional 에 해서 동작이 어떻게 되는지
어떤 기능들이 있는지 알아보았다. 어떻게 보면
기초가 되는 내용이라 늦기전에 정리해보는 것이 좋다고 생각이 들어
다행이라고 생각이 들었다.