20. 스프링 트랜잭션

20. 스프링 트랜잭션

스프링의 트랜잭션 개념과 속성, 애너테이션 기반 트랜잭션 적용 방법을 다룬다.


1. 트랜잭션이란?

트랜잭션(Transaction)은 여러 DML 명령문을 하나의 논리적 작업 단위로 묶어 관리하는 것이다. All or Nothing 원칙에 따라 모든 SQL이 성공하면 커밋(commit)하고, 하나라도 실패하면 전체를 롤백(rollback)한다.

  트랜잭션 처리 흐름

  SQL-1 실행
       │
       ▼
  SQL-2 실행
       │
       ▼
  SQL-3 실행
       │
       ▼
  ┌──────────────┐
  │ 모두 성공?    │
  └──────┬───────┘
    Yes  │  No
     │   │   │
     ▼   │   ▼
  COMMIT │ ROLLBACK

대표적인 예시: 계좌 이체

// 1. A 계좌에서 출금
updateBalance(accountA, -10000);
// 2. B 계좌에 입금
updateBalance(accountB, +10000);

1번은 성공했는데 2번에서 오류가 발생하면? 트랜잭션이 없으면 A의 돈만 빠져나간다. 트랜잭션으로 묶으면 두 작업 모두 롤백되어 데이터 정합성을 지킨다.

트랜잭션은 주로 Service 계층에서 적용한다. DAO 단위로 걸면 여러 DAO 호출을 하나의 작업 단위로 묶을 수 없다.

2. 스프링 트랜잭션 설정 방법

스프링에서 트랜잭션을 적용하는 방법은 두 가지다.

방식특징
XML 설정<tx:advice>, <aop:config>으로 선언
애너테이션@Transactional을 메서드/클래스에 부착

XML 방식은 설정이 복잡해지면 관리가 어려워 현재는 애너테이션 방식이 표준이다.

애너테이션 방식 설정

<!-- 트랜잭션 매니저 등록 -->
<bean id="transactionManager"
  class="org.springframework.jdbc
         .datasource
         .DataSourceTransactionManager">
    <property name="dataSource"
        ref="dataSource" />
</bean>

<!-- 애너테이션 기반 트랜잭션 활성화 -->
<tx:annotation-driven
    transaction-manager="transactionManager" />

Service에서 사용

@Service("orderService")
@Transactional
public class OrderServiceImpl
        implements OrderService {

    @Autowired
    private OrderDAO orderDAO;

    @Autowired
    private StockDAO stockDAO;

    @Override
    public void placeOrder(OrderVO order) {
        // 두 작업이 하나의 트랜잭션으로 묶임
        orderDAO.insertOrder(order);
        stockDAO.decreaseStock(
            order.getProductId(),
            order.getQuantity());
    }
}
@Transactional클래스에 붙이면 모든 public 메서드에 트랜잭션이 적용된다. 메서드에 붙이면 해당 메서드에만 적용된다.

3. 트랜잭션 속성

@Transactional에 설정할 수 있는 속성은 다음과 같다.

속성기능
propagation트랜잭션 전파 규칙 설정
isolation트랜잭션 격리 레벨 설정
readOnly읽기 전용 여부 설정
rollbackFor롤백할 예외 타입 설정
noRollbackFor롤백하지 않을 예외 타입 설정
timeout타임아웃 시간(초) 설정

4. propagation — 전파 규칙

트랜잭션이 이미 진행 중일 때 새로운 트랜잭션 요청을 어떻게 처리할지 결정한다.

동작
REQUIRED기존 트랜잭션 사용, 없으면 새로 생성 (기본값)
MANDATORY기존 트랜잭션 필수, 없으면 예외 발생
REQUIRES_NEW항상 새 트랜잭션 생성 (기존은 일시 중지)
SUPPORTS트랜잭션 불필요, 있으면 참여
NOT_SUPPORTED트랜잭션 불필요, 있으면 일시 중지
NEVER트랜잭션 불필요, 있으면 예외 발생
NESTED기존 트랜잭션 안에 중첩 트랜잭션 생성

자주 쓰는 전파 규칙 비교

  REQUIRED (기본값)

  ServiceA.method()
  ┌──── TX-1 ─────────┐
  │ ServiceB.method()  │
  │ (TX-1에 참여)       │
  └────────────────────┘

  REQUIRES_NEW

  ServiceA.method()
  ┌──── TX-1 ──────┐
  │  (일시 중지)     │
  └────────────────┘
  ┌──── TX-2 ──────┐
  │ ServiceB       │
  │  .method()     │
  └────────────────┘
  ┌──── TX-1 재개 ──┐
  └────────────────┘
// 로그는 별도 트랜잭션으로 저장
@Transactional(
    propagation = Propagation.REQUIRES_NEW)
public void saveLog(LogVO log) {
    logDAO.insertLog(log);
}
REQUIRES_NEW로그 저장, 알림 발송처럼 메인 트랜잭션 실패와 무관하게 반드시 기록해야 하는 작업에 사용한다.

5. isolation — 격리 레벨

동시에 여러 트랜잭션이 실행될 때 데이터 접근을 어느 수준까지 허용할지 결정한다.

레벨설명Dirty ReadNon-Repeatable ReadPhantom Read
DEFAULTDB 기본값 사용
READ_UNCOMMITTED커밋 안 된 데이터 읽기 허용OOO
READ_COMMITTED커밋된 데이터만 읽기XOO
REPEATABLE_READ같은 데이터를 반복 읽기 시 동일 보장XXO
SERIALIZABLE완전 직렬화 (성능 최저)XXX
격리 레벨이 높을수록 데이터 정합성은 좋아지지만 동시 처리 성능이 떨어진다. 대부분의 실무에서는 DB 기본값(DEFAULT)을 사용한다.

6. rollbackFor와 readOnly

rollbackFor

스프링은 기본적으로 **RuntimeException(Unchecked)**에서만 롤백한다. Checked Exception에서도 롤백하려면 명시해야 한다.

@Transactional(
    rollbackFor = Exception.class)
public void transferMoney(
        String from, String to, int amount)
        throws Exception {
    accountDAO.withdraw(from, amount);
    accountDAO.deposit(to, amount);
}
설정롤백 대상
기본값RuntimeException 및 하위 클래스
rollbackFor = Exception.class모든 예외에서 롤백
noRollbackFor = SomeException.class해당 예외는 롤백하지 않음

readOnly

조회 전용 메서드에 readOnly = true를 설정하면 불필요한 스냅샷 저장을 건너뛰어 성능이 향상된다.

@Transactional(readOnly = true)
public List<MemberVO> getAllMembers() {
    return memberDAO.selectAllMembers();
}
readOnly = true인 트랜잭션에서 INSERT/UPDATE/DELETE를 실행하면 DB에 따라 예외가 발생하거나 무시될 수 있다. 조회 메서드에만 사용하자.

7. 트랜잭션 적용 시 주의사항

같은 클래스 내부 호출 문제

스프링 @Transactional프록시 기반으로 동작한다. 같은 클래스 내부에서 메서드를 호출하면 프록시를 거치지 않아 트랜잭션이 적용되지 않는다.

@Service
public class MemberServiceImpl
        implements MemberService {

    // 트랜잭션 적용됨
    @Transactional
    public void methodA() {
        // 내부 호출 → 트랜잭션 미적용!
        methodB();
    }

    @Transactional(
        propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 새 트랜잭션이 시작되지 않는다
    }
}
  프록시 기반 트랜잭션

  외부 호출
       │
       ▼
  ┌──────────────┐
  │   Proxy      │
  │  (TX 시작)   │
  └──────┬───────┘
         │
         ▼
  ┌──────────────┐
  │  실제 객체    │
  │  methodA()   │
  │    ↓         │
  │  methodB()   │ ← 프록시 안 거침
  └──────────────┘
내부 호출 시 트랜잭션이 필요하면 별도의 빈으로 분리하거나, AopContext.currentProxy()를 사용해야 한다.

요약

개념한 줄 정리
트랜잭션여러 SQL을 하나의 작업 단위로 묶어 All or Nothing 처리
@Transactional스프링에서 트랜잭션을 선언적으로 적용하는 애너테이션
propagation기존 트랜잭션과의 관계를 결정 (기본값: REQUIRED)
isolation동시 트랜잭션 간 데이터 접근 수준 설정
rollbackForChecked Exception에서도 롤백하도록 지정
readOnly조회 전용 트랜잭션으로 성능 최적화

XML 방식 vs 애너테이션 방식

구분XML 방식애너테이션 방식
설정 위치스프링 설정 파일자바 클래스/메서드
가독성설정이 많아지면 복잡코드와 가까워 직관적
유연성AOP 패턴으로 일괄 적용 가능메서드별 세밀한 제어 가능
현재 추세레거시 프로젝트표준 (권장)