본문으로 건너뛰기
Advertisement

10.3 @Transactional 선언적 트랜잭션과 MyBatis 연동

통계나 단일 조회 기능을 제외하면, 현대 백엔드 애플리케이션의 거의 모든 비즈니스 수집/수정 로직은 내부적으로 여러 단계의 DB 상태 변경 작업(INSERT, UPDATE 등)을 거치게 됩니다.

1. 트랜잭션(Transaction)의 필요성

가령 "주문 결제" 서비스의 경우

  1. 회원의 잔고 차감 (UPDATE)
  2. 재고 감소 (UPDATE)
  3. 주문 영수증 기록 (INSERT)

이 일련의 과정 중 1, 2번은 성공했는데 서버에 장애가 생겨 3번에서 터졌다고 가정해 봅시다. 이미 차감된 잔금과 재고는 영영 미아 데이터가 됩니다. 따라서 이 세 작업은 전부 한꺼번에 성공하여 적용되거나(COMMIT), 하나라도 실패하면 시작 전 1번의 상태로 리셋(ROLLBACK) 되어야 합니다.

2. Spring + MyBatis 트랜잭션 동작 원리

스프링 부트에서 mybatis-spring-boot-starter를 사용하면, MyBatis의 SqlSession 작업이 스프링의 DataSourceTransactionManager 에 의해 자동으로 관리됩니다.

  • 동작 방식: @Transactional이 선언된 메서드가 호출되면, 스프링 AOP 프록시가 해당 요청을 가로채 DB 커넥션을 획득하고 트랜잭션을 시작합니다.
  • MyBatis 연동: MyBatis의 Mapper 메서드들이 실행될 때, 스프링이 관리하는 동일한 커넥션을 공유하여 실행됩니다. 작업이 끝나면 프록시가 커밋이나 롤백을 수행합니다.

3. 실전 예제 (MyBatis 연동)

@Service
@RequiredArgsConstructor
public class ShopService {

private final UserMapper userMapper; // MyBatis Mapper
private final ProductMapper productMapper; // MyBatis Mapper

@Transactional(rollbackFor = Exception.class)
public void buyItem(Long userId, Long productId, int quantity) {
// 1. 유저 잔고 차감 (MyBatis UPDATE)
userMapper.updateBalance(userId, -10000);

// 2. 상품 재고 차감 (MyBatis UPDATE)
int updatedRows = productMapper.updateStock(productId, -quantity);

// 비즈니스 예외 처리 (강제 롤백 유도)
if (updatedRows == 0) {
throw new RuntimeException("재고가 부족하여 주문할 수 없습니다.");
}
}
}

4. 트랜잭션 전파(Propagation)와 격리 수준(Isolation)

상황에 따라 트랜잭션의 범위를 세밀하게 조정할 수 있습니다.

속성설명비고
REQUIRED부모 트랜잭션이 있으면 합류, 없으면 새로 생성기본값 (가장 많이 사용)
REQUIRES_NEW부모와 상관없이 항상 새 트랜잭션 생성로그 기록 등 독립적 작업 시 사용
READ_COMMITTED다른 트랜잭션이 커밋한 데이터만 읽음격리 수준 기본값
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public void saveLog() { ... }

5. 기본 롤백 규칙과 주의점

스프링의 @Transactional은 AOP(관점 지향 프로그래밍) 프록시를 기반으로 동작하므로 무척 강력하지만, 주의해야 할 롤백 규칙이 있습니다.

  • 기본적으로 스프링은 Unchecked Exception(RuntimeException과 그 하위 예외) 및 Error가 튀어나왔을 때만 롤백을 수행합니다.
  • 만약 자바의 컴파일러가 강제하는 Checked Exception(예: IOException, SQLException)이 발생하면 롤백을 수행하지 않고 그대로 커밋(Commit)해 버립니다.

해결 방안:rollbackFor = Exception.class 옵션을 지정하여 모든 예외에 대해 롤백하도록 설정하거나, 비즈니스 예외를 런타임 예외로 설계합니다.

6. timeout과 readOnly (실무 활용)

장시간 걸리는 트랜잭션이 DB 커넥션을 붙잡지 않도록 timeout을 두고, 조회 전용 메서드는 readOnly로 표시하면 일부 DB/드라이버에서 읽기 부하 최적화가 가능합니다.

속성설명예시
timeout트랜잭션 최대 대기 시간(초). 초과 시 예외 발생.timeout = 5
readOnly읽기 전용 트랜잭션. 쓰기 시 예외. 힌트로 DB/드라이버 최적화 가능.readOnly = true
noRollbackFor지정 예외 시 롤백하지 않음. 비즈니스 예외로 “실패로 처리하되 DB는 유지”할 때 사용.noRollbackFor = BusinessException.class
@Transactional(readOnly = true, timeout = 10)
public List<OrderDto> findOrdersByUser(Long userId) {
return orderRepository.findByUserId(userId);
}

@Transactional(rollbackFor = Exception.class, timeout = 5, noRollbackFor = DuplicateOrderException.class)
public void createOrder(OrderRequest dto) {
// 중복 주문은 예외만 던지고 롤백하지 않을 때
}
Advertisement