Spring Boot를 사용하면 Transactional이라는 어노테이션을 자주 접하게 된다. @Transactional은 스프링 프레임워크에서 트랜잭션을 선언적으로 처리하기 위한 어노테이션이다. 이 어노테이션을 메서드 또는 클래스에 붙이면, 해당 범위 내에서 실행되는 로직은 트랜잭션으로 감싸지며, 성공하면 자동 커밋되고, 예외 발생 시 자동 롤백되는 기능을 수행한다.
Transactional 어노테이션의 역할을 알기 전, Transaction이 무엇인지 먼저 짚고 넘어가자.
Transaction(트랜잭션)
Transaction(트랜잭션)은 하나의 작업 단위(Unit of Work)를 구성하는 여러 개의 연산 집합(예: SELECT, INSERT, UPDATE, DELETE)으로, 모두 성공하거나 모두 실패해야 한다는 원칙을 가진다. 즉, 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 의미한다.
이 Transaction은 ACID(원자성, 일관성, 고립성, 지속성) 원칙을 따르며, 데이터 정합성을 보장하기 위해 반드시 필요한 개념이다. 이 ACID 원칙이라 불리는 네 가지 특징은 데이터 무결성과 일관성을 보장하기 위한 규칙이다.
ACID 원칙은 다음과 같다.
원자성(Atomicity)
원자성(Atomicity)은 트랜잭션은 더 이상 나눌 수 없는 최소 단위이다. 트랜잭션 내 모든 작업은 모두 수행되거나 모두 수행되지 않아야 한다.
A 계좌에서 10만원 출금 → B 계좌에 10만원 입금
→ 둘 중 하나라도 실패하면 전체 작업이 취소되어야 함
일관성(Consistency)
일관성(Consistency)은 트랜잭션 수행 전과 후에 데이터베이스는 항상 일관된 상태를 유지해야 한다. 즉, 제약조건(무결성 제약, 외래키, 도메인 조건 등)이 항상 만족되어야 한다.
은행 시스템에서 잔고의 합이 항상 일정해야 함
→ 트랜잭션 수행 후에도 제약조건이 깨지지 않아야 함
고립성(Isolation)
고립성(Isolation)은 여러 트랜잭션이 동시에 실행되더라도 각 트랜잭션은 독립적으로 실행되어야 한다. 다른 트랜잭션의 중간 결과에 영향을 받거나 보여서는 안 된다.
T1: A 계좌 잔고 조회 → 출금
T2: A 계좌 잔고 수정
→ 서로 영향을 주지 않고 순차적으로 실행된 것처럼 보여야 함
지속성(Durability)
지속성(Durability)은 트랜잭션이 성공적으로 커밋되면 그 결과는 영구적으로 데이터베이스에 저장되어야 한다. 시스템 오류나 장애가 발생해도 변경된 내용은 손실되지 않는다.
T1: 주문 완료 후 DB에 저장됨
→ 서버가 다운되더라도 주문 내역은 남아 있어야 함
코드 예시
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.withdraw(amount);
to.deposit(amount);
// 위 과정 전체가 하나의 트랜잭션
// 중간에 예외 발생 시 전체가 롤백됨
}
이 예시는 Atomicity와 Consistency를 중심으로, 스프링에서 트랜잭션의 개념이 어떻게 코드에 적용되는지를 보여준다.
@Transactional
위에서 설명했듯 Spring Framework에서 제공하는 @Transactional 어노테이션은 트랜잭션 처리를 선언적으로 제어할 수 있게 해주는 핵심 기능이다. 직접 트랜잭션을 시작하고 커밋하거나 롤백하는 로직을 작성하지 않아도, 어노테이션만으로 트랜잭션의 시작과 종료를 프레임워크가 대신 처리해 준다.
자주 사용되는 옵션들은 다음과 같다.
- readOnly: 읽기 전용 트랜잭션. Hibernate 플러시 전략 최적화
- rollbackFor: 롤백할 예외 클래스 지정
- noRollbackFor: 롤백 제외할 예외 지정
- propagation: 트랜잭션 전파 전략 설정 (REQUIRED, REQUIRES_NEW 등)
- isolation: 트랜잭션 격리 수준 설정 (READ_COMMITTED, SERIALIZABLE 등)
다음은 @Transactional이 수행하는 주요 역할들이다.
트랜잭션 자동 시작 및 커밋
- @Transactional이 붙은 메서드가 실행되면, 자동으로 트랜잭션이 시작된다.
- 메서드가 예외 없이 종료되면 트랜잭션은 자동 커밋된다.
@Transactional
public void updateUsername(Long userId, String newName) {
User user = em.find(User.class, userId); // 영속 상태
user.setUsername(newName); // 변경 (더티 상태)
// 트랜잭션 커밋 시 자동으로 UPDATE 쿼리 실행됨
}
위 코드는 @Transactional이 메서드에 선언되어 있어, 메서드 진입 시 트랜잭션이 자동 시작된다. 따라서 예외 없이 정상 종료되면 트랜잭션이 자동 커밋되어 변경된 필드가 DB에 반영된다.
예외 발생 시 롤백
- 기본 설정에서는 RuntimeException 또는 Error가 발생하면 자동으로 트랜잭션을 롤백한다.
- Checked Exception(예: IOException) 발생 시에는 기본적으로 롤백되지 않지만, rollbackFor 속성으로 롤백 대상을 지정할 수 있다.
@Transactional
public void withdrawPoint(Long userId, int amount) {
User user = em.find(User.class, userId);
if (user.getPoint() < amount) {
throw new IllegalArgumentException("포인트 부족"); // 런타임 예외 → 롤백됨
}
user.setPoint(user.getPoint() - amount); // 감산
}
위 예제는 IllegalArgumentException은 RuntimeException 계열이므로 트랜잭션이 자동 롤백된다. 그러므로 user.setPoint(...)가 수행되었더라도 DB에는 반영되지 않는다. 커밋이 정상적으로 수행되지 않기 때문이다.
더티 체킹(Dirty Checking) 연동
- 트랜잭션이 종료될 때, JPA가 엔티티의 변경 사항을 감지하여 자동으로 UPDATE 쿼리를 생성하고 실행한다.
이 메커니즘을 더티 체킹(Dirty Checking)이라고 한다.
@Transactional
public void changeEmail(Long userId, String newEmail) {
User user = em.find(User.class, userId); // 영속 상태
user.setEmail(newEmail); // 필드 수정 (하지만 DB에는 아직 반영되지 않음)
// 트랜잭션 커밋 시 Hibernate가 스냅샷과 비교 → UPDATE 실행
}
위 예시는 Hibernate는 user 객체의 초기 상태를 기억해두고 있다가, 트랜잭션 커밋 시점에 변경 사항을 감지하는 내용이다.
이 때, UPDATE user SET email=? WHERE id=? 쿼리를 자동 생성 및 실행한다. 즉, 개발자가 직접 em.update() 등을 호출하지 않아도 된다.
예제: 사용자 포인트 충전 서비스
다음은 @Transactional의 세 가지 핵심 기능을 하나의 예제 코드 흐름으로 통합하여 보여주는 Spring Boot + JPA 기반 예시이다.
@Service
public class PointService {
private final EntityManager em;
public PointService(EntityManager em) {
this.em = em;
}
@Transactional
public void chargePoint(Long userId, int amount) {
// 트랜잭션 자동 시작됨
User user = em.find(User.class, userId); // 1. 영속 상태로 조회
if (user == null) throw new IllegalArgumentException("사용자 없음");
// 2. 값 변경 → 더티 체킹 대상
user.setPoint(user.getPoint() + amount);
// 3. 예외 조건 예시
if (amount < 0) {
throw new IllegalArgumentException("충전 금액은 0 이상이어야 합니다.");
}
// 트랜잭션 종료 시점 → 커밋 or 롤백
}
}
이 커밋을 진행하게 되면 다음 쿼리가 실행되는 것으로 기대할 수 있다.
update user set point = ? where id = ?
위 코드 흐름에서 예외 없이 종료되면 위와 같은 UPDATE 쿼리가 자동 실행된다. 또한 예외 발생 시에는 트랜잭션이 롤백되므로 이 쿼리는 실행되지 않는다.
관련 포스팅
참고 자료
https://joojimin.tistory.com/68
https://resilient-923.tistory.com/415