이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › Spring
@Transactional은 무엇을 묶어 주는가
@Transactional은 메서드 위에 적는 표시(애너테이션)입니다. 이 실습의 order() 메서드는 jdbc.update로 DB에 저장을 두 번 합니다(orders 테이블에 주문, payment 테이블에 결제). 두 저장 사이에는 stock 확인이 있고, stock이 0이면 예외를 던집니다. 아무 표시가 없으면 DB는 저장을 받는 대로 반영하므로, 예외가 나도 앞의 저장은 그대로 남습니다. order() 위에 @Transactional을 적으면 스프링이 그 메서드 안의 저장을 한 묶음(트랜잭션)으로 다루어, 메서드가 정상으로 끝나면 모두 반영(커밋)하고 런타임 예외로 끝나면 모두 취소(롤백)합니다. 묶음을 시작하고 반영하거나 취소하는 코드는 우리가 쓰지 않고, 스프링이 order()를 부르는 앞뒤에서 합니다. 이 실습은 @Transactional을 지우거나 stock에 담는 값을 바꿔 가며 같은 order()를 부르고, 호출이 끝난 뒤 두 테이블에 남은 행을 봅니다.
실행으로 대조 · Spring 7.0.9와 H2 2.4.240에서 네 가지 조합을 실제 @Transactional 프록시로 실행해 행 수와 단계 문장의 근거를 대조
@Service
public class OrderService {
private final JdbcTemplate jdbc;
public OrderService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
// 묶음: 이 메서드 안의 저장은 모두 반영되거나 모두 취소된다
@Transactional
public void order() {
jdbc.update("INSERT INTO orders VALUES (1, '키보드')");
int stock = 0;
if (stock == 0) {
throw new IllegalStateException("재고 없음");
}
jdbc.update("INSERT INTO payment VALUES (1, 30000)");
}
}
Spring Framework 7.0.9를 JDK 21에서, H2 2.4.240(메모리 DB)에 JDBC로 연결해 실행해 대조했고 MySQL에서의 실행 대조는 하지 않았습니다. 네 가지 조합을 모두, 화면의 OrderService와 같은 소스(package와 import 줄, 그리고 끝에 PROBE 표시를 붙인 측정 줄 두 개만 더함)를 컴파일하고 자바 프로세스를 조합마다 새로 띄워 확인했습니다. 설정 클래스에는 AnnotationConfigApplicationContext가 읽는 @EnableTransactionManagement, H2의 DataSource(HikariCP), JdbcTemplate, DataSourceTransactionManager(시작, 반영, 취소 횟수를 세는 줄만 더한 하위 클래스)를 두었고, OrderService는 @Service로 스프링이 컴포넌트 스캔으로 찾게 했습니다. 자동 커밋은 JDBC 기본값(켜짐) 그대로이고 조합마다 DB와 두 테이블(orders, payment)을 새로 만들었습니다. 대조한 것은 order()가 끝난 방식(IllegalStateException과 그 메시지, 또는 정상)과, 호출이 끝난 뒤 별도의 조회로 센 orders와 payment의 행 수입니다. 단계 문장이 기대는 사실로 order() 안에서 트랜잭션이 활성인지(@Transactional이 있을 때만 활성), main이 받은 OrderService 빈의 클래스(@Transactional이 있으면 스프링이 만든 CGLIB 프록시 클래스, 없으면 우리가 쓴 클래스 그대로), 첫 저장 직후 따로 연결한 DB 연결에서 센 orders의 행 수(@Transactional이 있으면 0행, 없으면 1행), 트랜잭션 매니저가 묶음을 시작하고 반영하고 취소한 횟수(예외로 끝나면 시작 1번과 취소 1번, 정상으로 끝나면 시작 1번과 반영 1번, @Transactional이 없으면 모두 0번)도 실행으로 확인했습니다. 둘째 저장이 묶음 안에서 확정되지 않는다는 것과 없을 때 곧바로 반영된다는 것은 첫 저장으로 잰 값과 최종 행 수에서 이끌어 낸 것이고, 둘째 저장 직후를 따로 재지는 않았습니다. 문서가 정한 것은 스프링 프레임워크 7.0 문서 Data Access의 Declarative Transaction Management 절(선언적 트랜잭션이 AOP로 이루어진다는 것), Understanding the Spring Framework's Declarative Transaction Implementation 절(TransactionInterceptor와 트랜잭션 매니저가 메서드 호출 둘레에서 트랜잭션을 이끈다는 것), Using @Transactional 절(@EnableTransactionManagement로 켜야 하고 기본은 프록시 방식이라는 것, 기본으로 RuntimeException과 Error는 롤백하고 체크 예외는 롤백하지 않는다는 것), Rolling Back a Declarative Transaction 절(기본 설정에서 런타임 예외에만 롤백 표시를 한다는 것)입니다. 다만 메서드 앞에서 시작하고 정상이면 반영하며 예외면 취소하는 순서를 문서가 한 문단으로 풀어 쓰고 있지는 않아서, 그 순서는 Probe가 센 횟수와 행 수로 확인한 관찰입니다. 화면의 코드는 package와 import 줄을 줄였고, order()를 부르는 main, 테이블을 만드는 CREATE TABLE, DB 연결과 스프링 설정은 보이지 않습니다. 이 실습은 RuntimeException이 던져지는 한 가지 예외만 다루며, 체크 예외, 같은 클래스 안에서 부르는 경우, readOnly는 다루지 않습니다. 체크 예외와 rollbackFor, 전파 방식은 "트랜잭션 전파" 실습에서, 트랜잭션 자체(격리 수준 포함)는 "트랜잭션 기본" 실습에서 다룹니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.