트랜잭션 롤백의 예상치 못한 동작 이해하기 🤔

궁금하면 500원·2025년 2월 1일

미생의 스프링

목록 보기
30/48

트랜잭션 롤백이 항상 예상대로 동작할까요?🧐

오늘은 Checked Exception과 Unchecked Exception의 차이, 그리고 프록시 구조에 따라 롤백이 예상과 다르게 동작하게되는 경우에 대해 알아보겠습니다!

📚 트랜잭션과 롤백의 기본 개념

스프링의 @Transactional 어노테이션을 사용하면 예외 발생 시 트랜잭션이 롤백됩니다.
Unchecked Exception은 자동으로 롤백되지만, Checked Exception은 기본적으로 롤백되지 않으며, rollbackFor 속성을 지정해야 합니다.

🎯 Checked Exception과 예외 처리의 복잡성

Java에서는 Checked Exception이 발생해도 기본적으로 롤백되지 않지만, Kotlin에서는 Checked Exception을 강제하지 않아 코드에 따라 동작이 달라질 수 있습니다.
특히 Java-Kotlin 혼합 프로젝트에서는 UndeclaredThrowableException으로 변환될 수 있어 주의가 필요합니다.

📊 실제 사례: 효율적인 트랜잭션 관리

트랜잭션을 장시간 유지하면 성능 저하가 발생할 수 있어, 트랜잭션을 분리하여 관리하는 전략이 필요합니다.
예를 들어, 카프카를 활용한 무손실 이벤트 처리에서는 트랜잭션을 짧게 유지하는 방식이 효과적입니다.

💡 리뷰어 한줄평

스프링의 @Transactional 어노테이션을 사용하다 보면 때로는 예상과 다르게 동작하는 경우를 마주치게 됩니다.
특히 예외 처리와 관련하여 롤백이 발생하지 않거나, 의도치 않게 발생하는 경우가 있어 주의가 필요합니다.

트랜잭션 롤백의 기본 동작

Checked vs Unchecked Exception

스프링에서 트랜잭션 롤백은 기본적으로 다음과 같이 동작합니다.

  • Unchecked Exception (RuntimeException): 자동 롤백
  • Checked Exception: 롤백되지 않음 (rollbackFor 속성으로 지정 필요)
@Transactional
public void processOrder() {
    // RuntimeException 발생 시 자동 롤백
    throw new RuntimeException("주문 처리 실패");
    
    // IOException은 롤백되지 않음
    throw new IOException("파일 처리 실패");
}

예상과 다른 롤백 동작 사례

1. 내부 메서드 호출과 프록시

@Service
class OrderService {
    fun process() {
        save() // @Transactional이 동작하지 않음
    }

    @Transactional
    fun save() {
        // 데이터 저장
    }
}

위 코드에서 save() 메서드의 @Transactional은 동작하지 않습니다.
이는 스프링 AOP가 프록시 방식으로 동작하기 때문입니다.

2. 예외 처리와 롤백 마킹

@Service
class OrderService {
    @Transactional
    fun process() {
        try {
            paymentService.process() // REQUIRES_NEW로 설정된 트랜잭션
        } catch (e: Exception) {
            // 예외 처리
        }
    }
}

REQUIRES_NEW로 설정된 트랜잭션에서 발생한 예외는 해당 트랜잭션만 롤백하고, 부모 트랜잭션에는 영향을 주지 않습니다.

실무 적용 사례: 대량 데이터 처리

대량의 데이터를 처리할 때는 트랜잭션을 적절히 분리하는 것이 중요합니다.

@Service
class DataProcessor {
    fun processLargeData() {
        while (true) {
            transactionTemplate.execute {
                val batch = repository.findBatch(BATCH_SIZE)
                if (batch.isEmpty()) {
                    return@execute
                }
                processBatch(batch)
            }
        }
    }
}

이렇게 배치 크기를 제한하고 각각의 처리를 개별 트랜잭션으로 실행하면 메모리 사용량을 줄이고 성능을 개선할 수 있습니다.

트랜잭션 관리의 모범 사례

1. 트랜잭션 범위 최소화

  • 트랜잭션은 필요한 최소한의 범위로 제한
  • 긴 트랜잭션은 데이터베이스 리소스 낭비의 원인

2. 예외 처리 전략

  • Checked Exception과 Unchecked Exception의 특성 이해
  • 상황에 맞는 예외 처리 방식 선택

3. 트랜잭션 전파 설정

  • REQUIRED, REQUIRES_NEW 등의 전파 옵션 적절히 활용
  • 중첩 트랜잭션의 동작 방식 이해

결론

트랜잭션의 롤백 동작을 정확히 이해하고 적용하는 것은 안정적인 애플리케이션 개발의 기본이라는것을 이번에 배우게 되었습니다.
특히 대용량 데이터를 다루는 경우, 트랜잭션 관리는 성능과 안정성에 직접적인 영향을 미치므로 신중한 설계가 필요합니다.

느낀점

트랜잭션 관리는 백엔드 개발에서 가장 기본이 되는 부분임에도 불구하고, 실제로 다양한 상황에서 예상과 다르게 동작할 수 있다는 점을 다시 한번 깨달았습니다.

특히 Kotlin과 Java를 함께 사용하는 프로젝트에서는 예외 처리와 관련하여 더욱 신중한 접근이 필요하다는 것을 배웠습니다.

이번 포스팅을 통해 트랜잭션의 기본 개념부터 실무에서 마주칠 수 있는 다양한 케이스들을 정리하면서, 더 안정적인 애플리케이션을 만들기 위한 인사이트를 얻을 수 있었습니다.

참고 URL:

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글