Transaction이란?
일단 트랜잭션이라는 개념은 데이터베이스에서의 연관된 일련의 작업단위이다.
여러 작업을 하나의 묶음으로 처리하여 모두 성공하거나, 모두 실패하게 만드는 기능입니다.
트랜잭션은 더 이상 쪼갤 수 없는 일련의 최소 작업 단위입니다.
ex:
[계좌 A → 계좌 B로 10만원 이체]
1. 계좌 A에서 10만원 차감
2. 계좌 B에 10만원 입금
이러한 경우는 A에서 차감과 B에서 추가 이 두 동작이 하나의 트랜잭션으로 묶이는 것이다.
작업 도중 하나의 트랜잭션 중간에 예외가 발생한다면 안전하게 이전상태로 롤백이 되어야 할 것입니다.
이러한 트랜잭션 마무리 작업에는 크게 트랜잭션 커밋, 트랜잭션 롤백이 있습니다.
트랜잭션 커밋, 롤백
트랜잭션 안에서 수행한 모든 변경사항을 DB에 영구 반영하는 작업
커밋된 트랜잭션은 이후 롤백 할 수 없습니다.
- 스프링에서 자동 커밋
Spring이 트랜잭션을 시작 → 성공 시 자동 커밋, 실패 시 자동 롤백합니다.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional // ✔ 자동 트랜잭션 관리
public void placeOrder(String itemName) {
Order order = new Order();
order.setItemName(itemName);
orderRepository.save(order); // 자동 커밋 대상
if ("fail".equals(itemName)) {
throw new RuntimeException("강제 예외 발생!");
}
}
}
- 스프링에서 수동 커밋
수동 방식은 직접 트랜잭션 시작, 커밋 또는 롤백을 명시합니다. PlatformTransactionManager 을 활용하여 구현합니다.
* PlatformTransactionManager : 트랜잭션을 추상화해서 다루는 핵심 인터페이스
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class ManualTxService {
private final OrderRepository orderRepository;
private final PlatformTransactionManager transactionManager;
public ManualTxService(OrderRepository orderRepository, PlatformTransactionManager transactionManager) {
this.orderRepository = orderRepository;
this.transactionManager = transactionManager;
}
public void placeOrderManually(String itemName) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Order order = new Order();
order.setItemName(itemName);
orderRepository.save(order); // 커밋 전에는 임시 상태
if ("fail".equals(itemName)) {
throw new RuntimeException("강제 예외 발생!");
}
transactionManager.commit(status); // 수동 커밋
System.out.println("✅ 트랜잭션 커밋 완료");
} catch (Exception e) {
transactionManager.rollback(status); // 수동 롤백
System.out.println("⛔ 트랜잭션 롤백");
throw e;
}
}
}
PlatformTransactionManager
트랜잭션을 시작, 커밋, 롤백할 수 있게 해주는 Spring의 트랜잭션 관리 인터페이스
주요 메서드
| 메서드 | 설명 |
| getTransaction() | 트랜잭션 시작 (TransactionStatus 반환) |
| commit() | 트랜잭션 커밋 |
| rollback() | 트랜잭션 롤백 |
Java에는 Connection.setAutoCommit(false) 등 직접 JDBC 트랜잭션만 있고,
JPA, Hibernate, JMS 등은 전부 방식이 다릅니다.
Spring은 이걸 하나의 인터페이스로 추상화합니다 (PlatformTransactionManager)
구현체 종류
| 구현체 | 용도 |
| DataSourceTransactionManager | JDBC, MyBatis |
| JpaTransactionManager | JPA, Hibernate |
| HibernateTransactionManager | Hibernate 전용 (비추천) |
| ChainedTransactionManager | 다중 DB 트랜잭션 |
| JtaTransactionManager | 글로벌 트랜잭션 (2PC 등) |
Spring Boot에서는 사용 중인 기술에 따라 자동으로 적절한 구현체가 등록
@Transactional 내부에서 PlatformTransactionManager가 어떻게 작동하는가?
Client → @Transactional 메서드 호출
↓
Spring AOP 프록시가 가로챔
↓
TransactionInterceptor 가 동작
↓
PlatformTransactionManager.getTransaction()
↓
→ 실제 메서드 실행
→ 성공 → PlatformTransactionManager.commit()
→ 예외 → PlatformTransactionManager.rollback()
TransactionStatus status = txManager.getTransaction(...); // 트랜잭션 시작
try {
Object result = joinPoint.proceed(); // 원래 메서드 실행
txManager.commit(status); // 성공 시 커밋
return result;
} catch (Throwable ex) {
txManager.rollback(status); // 예외 시 롤백
throw ex;
}
@Transactional 이 이 과정을 자동으로 감싸주는 것
Spring이 제공하는 Trasnaction 핵심 기술
1. 트랜잭션 동기화
실행 중인 트랜잭션에 대해 스레드 기준으로 연결(Connection)과 상태를 관리하고 공유하는 메커니즘
트랜잭션 동기화 저장소는 작업 쓰레드 마다 Connection 객체를 독립적으로 관리하기 때문에,
멀티쓰레드 환경에서도 충돌 발생 여지가 없다.
Spring에서는 여러 DAO나 Repository가 동시에 사용되는데,
→ 그들이 같은 트랜잭션 안에서 동일한 커넥션(Connection)을 공유해야 합니다.
@Transactional
public void placeOrder() {
orderRepository.save(...); // 🧾 계산서에 1건 추가
paymentRepository.save(...); // 🧾 같은 계산서에 또 추가
deliveryRepository.save(...); // 🧾 여기도 추가
}
이 세줄은 모두 같은 트랜잭션 안에서 실행되는데 Spring이 내부에서 "야 너희 같은 트랜잭션이야~" 하고 묶어주는 게 동기화
Spring에서는 여러 DAO나 Repository가 동시에 사용되는데,
→ 그들이 같은 트랜잭션 안에서 동일한 커넥션(Connection)을 공유해야 합니다.
동기화 없으면:
- Repository A는 conn1 사용
- Repository B는 conn2 사용
- → 커밋이 각각 따로 일어남 → 트랜잭션 분리됨 → 💥 데이터 불일치 위험
동기화 있으면:
- A, B가 같은 커넥션을 공유하게 됨 → 하나의 트랜잭션으로 묶임
2. 트랜잭션 추상화
DB, JPA, JMS 등 여러 리소스들의 트랜잭션을 하나의 통일된 방식으로 관리할 수 있도록
Spring이 제공하는 표준 인터페이스와 구조
이로써 애플리케이션에 각 기술마다 코드가 종속적이지 않고도 일관되게 트랜잭션 처리가 가능해진다.
위에서 언급한 PlatformTransactionManager가 대표적인 예시다.
사용 기술과 무관하게 PlatformTransactionManager를 통해 트랜잭션 관리가 가능해진다.
@Service
public class SimpleTransactionService {
private final JdbcTemplate jdbcTemplate;
private final PlatformTransactionManager transactionManager;
public SimpleTransactionService(JdbcTemplate jdbcTemplate,
PlatformTransactionManager transactionManager) {
this.jdbcTemplate = jdbcTemplate;
this.transactionManager = transactionManager;
}
public void saveBookWithTransaction(boolean fail) {
// 1. 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 2. 작업 수행
jdbcTemplate.update("INSERT INTO book (title) VALUES (?)", "Spring 트랜잭션");
if (fail) {
throw new RuntimeException("일부러 실패 발생!");
}
// 3. 커밋
transactionManager.commit(status);
System.out.println("✅ 커밋 완료");
} catch (Exception e) {
// 4. 롤백
transactionManager.rollback(status);
System.out.println("⛔ 롤백 완료");
}
}
}
하지만 이러한 코드는 비지니스 로직과 트랜잭션 코드가 혼합되어 있어 너무 길고 복잡하다
그래서 Spring에서는 AOP를 이용하여 관심사를 분리하였다.
3. AOP를 이용한 트랜잭션 분리
-적용전
@Service
public class MemberService {
private final DataSource dataSource;
private final JdbcTemplate jdbc;
public MemberService(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbc = new JdbcTemplate(dataSource);
}
public void joinMember(String name) throws SQLException {
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
// 회원 저장
jdbc.update("INSERT INTO member(name) VALUES (?)", name);
// 로그 저장 (예외 발생 가능)
jdbc.update("INSERT INTO log(message) VALUES (?)", "가입: " + name);
conn.commit();
System.out.println("✅ 수동 커밋 완료");
} catch (Exception e) {
conn.rollback();
System.out.println("⛔ 롤백됨: " + e.getMessage());
} finally {
conn.close();
}
}
}
위 코드는 너무 길기도 하고 트랜잭션 수동 관리의 번거로움도 존재한다. 그리고 가독성이 떨어진다 (비지니스 로직 + 트랜잭션 처리 로직)
그래서 스프링에서 트랜잭션 코드 같은 비지니스 로직외의 부가 코드는 존재하지 않는 것처럼 클래스 밖으로 빼서 별도의
모듈로 만드는 AOP를 적용하게 되었다.
이를 적용하여 만든 어노테이션이 @Transactional 이다.
-적용후
@Service
public class MemberService {
private final JdbcTemplate jdbc;
public MemberService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Transactional
public void joinMember(String name) {
// 회원 저장
jdbc.update("INSERT INTO member(name) VALUES (?)", name);
// 로그 저장 (예외 발생 가능)
jdbc.update("INSERT INTO log(message) VALUES (?)", "가입: " + name);
System.out.println("✅ 자동 커밋 또는 롤백됨");
}
}
코드 길이가 많이줄고 트랜잭션 관리도 알아서 해준다. 너무 편하다 ..... 비지니스 로직만 남게 되어서 좋다.
트랜잭션 사용시 주의사항
1. 프록시 내부 메서드 호출 (자기 자신 호출)
Spring의 트랜잭션은 프록시 기반 AOP로 동작하기 때문에 같은 클래스 내부의 메서드끼리 호출하면 트랜잭션이 적용되지 않는다.
@Service
public class OrderService {
@Transactional
public void outer() {
inner(); // ❌ 트랜잭션 적용 안 됨!
}
@Transactional
public void inner() {
// 실제로는 트랜잭션이 없음
}
}
2. 체크 예외 발생시 롤백이 안된다.
기본적으로 @Transactional 은 언체크 예외만 롤백합니다. 체크예외는 롤백되지 않습니다.
@Transactional
public void doSomething() throws IOException {
throw new IOException("체크 예외"); // ❌ 롤백 안 됨
}
//해결
@Transactional(rollbackFor = IOException.class)
public void doSomething() throws IOException {
throw new IOException("체크 예외");
}
3. 코드에서 예외를 try-catch 로 삼켜버리는 경우
@Transactional
public void outer() {
try {
inner(); // 예외 발생
} catch (Exception e) {
log.warn("예외 무시"); // ❌ 예외가 밖으로 안 나가서 롤백 안 됨
}
}
public void inner() {
throw new RuntimeException("실패");
}
트랜잭션은 예외가 밖으로 전달되어야 롤백한다.
예외를 catch 한후 아무것도 안하면 트랜잭션은 정상 처리 (커밋) 됩니다.
'Spring' 카테고리의 다른 글
| [Spring] 트랜잭션 전파 (1) | 2025.06.29 |
|---|---|
| [Spring] 체크예외 / 언체크 예외 (0) | 2025.06.29 |
| [Spring] Datasource (0) | 2025.06.29 |
| [Spring] 서블릿 예외 처리 , 스프링 부트 예외 처리 (0) | 2025.06.28 |
| [Spring] Interceptor, filter (0) | 2025.06.28 |