Categories
웹개발/Spring
Spring @Transactional 기본 정리 (propagation, isolation, rollback)
@Transactional이 하는 일
@Transactional은 메서드 실행을 하나의 트랜잭션으로 묶어, 전부 성공하면 커밋, 중간에 실패하면 롤백되게 한다. 예를 들어 주문을 저장하고 재고를 차감하는 두 작업이 한 메서드 안에 있다면, 둘 다 성공해야 반영되고 하나라도 실패하면 전체가 취소돼야 한다. 이 "모 아니면 도"를 보장하는 것이 트랜잭션이다. Spring @Transactional 사용법의 출발점은 이 메서드 단위 묶음을 이해하는 것이다.
사용은 간단하다. 보통 서비스 계층 메서드에 붙인다. 문제는 옵션(propagation, isolation, rollback)의 의미와, "분명 붙였는데 롤백이 안 되는" 함정이다. 운영 데이터 정합성이 걸리는 부분이라 동작 원리를 알고 써야 한다.
@Service
public class OrderService {
@Transactional // 이 메서드 전체가 하나의 트랜잭션
public void order(OrderVO order) {
orderMapper.insertOrder(order); // (1) 주문 저장
stockMapper.decrease(order.getItemId(), order.getQty()); // (2) 재고 차감
// (2)에서 예외가 나면 (1)도 롤백
}
}
주요 옵션 한눈에 보기
| 옵션 | 역할 | 자주 쓰는 값 |
|---|---|---|
| propagation | 트랜잭션 전파 방식 | REQUIRED(기본), REQUIRES_NEW |
| isolation | 격리 수준(동시성 처리) | DEFAULT(DB 기본), READ_COMMITTED |
| rollbackFor | 롤백할 예외 지정 | Exception.class 등 |
| readOnly | 읽기 전용 힌트 | 조회 전용 메서드에 true |
| timeout | 제한 시간(초) | 장기 트랜잭션 방지 |
propagation: 트랜잭션 전파
1. REQUIRED (기본값)
propagation은 이미 진행 중인 트랜잭션이 있을 때 어떻게 할지를 정한다. 기본값인 REQUIRED는 "진행 중인 트랜잭션이 있으면 거기 참여하고, 없으면 새로 만든다"이다. 대부분의 경우 이 기본값으로 충분하다.
@Transactional // propagation = REQUIRED (기본)
public void order(OrderVO order) {
orderMapper.insertOrder(order);
paymentService.pay(order); // 이 안의 @Transactional도 같은 트랜잭션에 참여
}
위에서 paymentService.pay()도 @Transactional이라면, 새 트랜잭션을 만들지 않고 바깥 트랜잭션에 합류한다. 그래서 어느 한쪽이 실패하면 전체가 함께 롤백된다.
2. REQUIRES_NEW
REQUIRES_NEW는 항상 새 트랜잭션을 만든다. 바깥 트랜잭션이 있어도 잠시 멈추고 독립된 트랜잭션으로 처리한다. 바깥이 롤백돼도 이쪽은 별도로 커밋될 수 있다. 로그 기록처럼 "본 작업과 무관하게 남겨야 하는" 경우에 쓴다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog(LogVO log) {
logMapper.insert(log); // 바깥이 롤백돼도 로그는 남기고 싶을 때
}
다만 REQUIRES_NEW는 트랜잭션을 분리하므로 동작을 정확히 이해하고 써야 한다. 무심코 쓰면 의도와 달리 일부만 커밋돼 데이터가 어긋날 수 있다.
isolation: 격리 수준
isolation은 동시에 여러 트랜잭션이 같은 데이터를 다룰 때 서로의 변경을 어디까지 보게 할지를 정한다. 격리 수준이 높을수록 데이터 일관성은 올라가지만 동시성(성능)은 떨어진다. 기본값은 DEFAULT로, DB가 정한 기본 격리 수준을 따른다. Oracle은 보통 READ COMMITTED가 기본이다.
// 대부분은 DEFAULT(DB 기본)로 두고, 특별한 경우만 조정
@Transactional(isolation = Isolation.READ_COMMITTED)
public void process() { ... }
실무에서는 격리 수준을 직접 바꾸는 일이 많지 않다. DB 기본값으로 두는 경우가 대부분이고, 동시성 이슈(같은 데이터 동시 수정 등)가 명확할 때만 신중히 조정한다. 격리 수준을 높이면 락 경합이나 성능 저하로 이어질 수 있어, 운영에서는 영향 범위를 충분히 검토하고 적용한다.
rollback: 언제 롤백되는가 (가장 중요한 함정)
1. 기본은 RuntimeException만 롤백
가장 많이 걸리는 함정이다. @Transactional은 기본적으로 RuntimeException(언체크 예외)과 Error에서만 롤백한다. 즉 IOException 같은 checked exception은 기본 설정으로는 롤백하지 않는다. 예외가 났는데도 데이터가 커밋돼 버리는 황당한 상황이 여기서 나온다.
// checked exception은 기본적으로 롤백되지 않는다
@Transactional
public void process() throws Exception {
orderMapper.insertOrder(order);
if (somethingWrong) {
throw new Exception("실패"); // checked → 롤백 안 됨, insert는 커밋됨
}
}
2. rollbackFor로 명시하기
checked exception에서도 롤백하려면 rollbackFor로 명시한다. 실무에서는 모든 예외에 롤백을 거는 경우가 많다.
// 모든 Exception에 대해 롤백
@Transactional(rollbackFor = Exception.class)
public void process() throws Exception {
orderMapper.insertOrder(order);
if (somethingWrong) {
throw new Exception("실패"); // 이제 롤백됨
}
}
또 하나 흔한 실수는 메서드 안에서 try-catch로 예외를 잡아 삼키는 것이다. 예외를 잡아서 밖으로 안 던지면 스프링은 정상 종료로 보고 커밋한다. 롤백이 필요하면 예외를 다시 던지거나, 명시적으로 롤백 처리를 해야 한다.
@Transactional이 아예 동작 안 할 때
옵션 이전에, @Transactional 자체가 안 먹는 경우가 있다. 원리를 모르면 한참 헤맨다.
- 같은 클래스 내부 호출(self-invocation) — 같은 클래스의 메서드를
this.method()로 직접 부르면 프록시를 거치지 않아 @Transactional이 적용되지 않는다. 별도 빈으로 분리하거나 구조를 바꿔야 한다. - private 메서드 — 프록시 기반 AOP라 private에는 적용되지 않는다. public 메서드에 붙인다.
- 예외를 try-catch로 삼킴 — 예외가 밖으로 안 나가면 롤백되지 않는다.
- 트랜잭션 매니저 미설정 — DataSourceTransactionManager 등 트랜잭션 매니저 빈이 등록돼 있고
<tx:annotation-driven>(또는 @EnableTransactionManagement)이 있는지 확인한다.
<!-- XML 설정 예: 트랜잭션 매니저 + 어노테이션 활성화 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />
실무에서 자주 하는 실수
- checked exception에서 롤백을 기대하지만
rollbackFor를 안 줘서 커밋되는 경우. - 메서드 안에서 예외를 try-catch로 잡아 삼켜 롤백이 안 되는 경우.
- 같은 클래스 내부 호출이라 @Transactional이 적용 안 되는 경우.
- 조회 전용 메서드에 readOnly를 안 줘서 불필요한 트랜잭션 오버헤드가 생기는 경우(성능 측면).
- 운영에서 격리 수준을 검증 없이 높여 락 경합·성능 저하가 발생하는 경우.
FAQ
Q1. 예외가 났는데 롤백이 안 됩니다.
checked exception은 기본적으로 롤백되지 않는다. @Transactional(rollbackFor = Exception.class)로 명시하거나, 예외를 try-catch로 삼키지 않았는지 확인한다.
Q2. propagation 기본값은 무엇인가요?
REQUIRED다. 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로 만든다. 대부분 이 기본값으로 충분하다.
Q3. 같은 클래스의 메서드를 호출했더니 트랜잭션이 안 먹습니다.
같은 클래스 내부 호출은 프록시를 거치지 않아 @Transactional이 적용되지 않는다. 해당 메서드를 다른 빈으로 분리하는 방식을 검토한다.
Q4. readOnly=true는 무슨 의미인가요?
읽기 전용 트랜잭션이라는 힌트다. 조회 전용 메서드에 주면 일부 최적화가 가능하다. 다만 동작은 DB·드라이버에 따라 다를 수 있다.
Q5. 격리 수준은 보통 어떻게 두나요?
대부분 DEFAULT(DB 기본값)로 둔다. Oracle은 보통 READ COMMITTED다. 동시성 문제가 명확할 때만 신중히 조정한다.
정리
Spring @Transactional 사용법의 핵심은 "메서드를 하나의 트랜잭션으로 묶고, 옵션으로 전파·격리·롤백을 제어하는 것"이다. propagation 기본값 REQUIRED는 진행 중 트랜잭션에 참여하고, isolation은 보통 DB 기본값으로 둔다. 가장 주의할 점은 rollback이다. 기본적으로 RuntimeException만 롤백되므로 checked exception까지 롤백하려면 rollbackFor를 명시하고, 예외를 try-catch로 삼키지 않아야 한다. 또 같은 클래스 내부 호출이나 private 메서드에서는 @Transactional이 적용되지 않는다는 점도 기억한다. 운영 데이터 정합성이 걸린 만큼, 격리 수준 조정이나 트랜잭션 구조 변경은 테스트 환경에서 충분히 확인한 뒤 반영하는 것이 안전하다.
'웹개발 > Spring' 카테고리의 다른 글
| session attribute에 loginUserVO 담고 꺼내기 정리 (1) | 2026.07.04 |
|---|---|
| Session 기반 로그인 HttpSession 동작 흐름 정리 (0) | 2026.07.03 |
| Spring @RequestParam으로 요청 파라미터 받기 정리 (0) | 2026.07.02 |
| Spring *.do URL 패턴 정리, 공공 프로젝트는 왜 .do로 끝날까 (0) | 2026.07.02 |
| @RequestMapping @GetMapping 차이 (0) | 2026.07.01 |