
오늘은 Checked Exception과 Unchecked Exception의 차이, 그리고 프록시 구조에 따라 롤백이 예상과 다르게 동작하게되는 경우에 대해 알아보겠습니다!
스프링의 @Transactional 어노테이션을 사용하면 예외 발생 시 트랜잭션이 롤백됩니다.
Unchecked Exception은 자동으로 롤백되지만, Checked Exception은 기본적으로 롤백되지 않으며, rollbackFor 속성을 지정해야 합니다.
Java에서는 Checked Exception이 발생해도 기본적으로 롤백되지 않지만, Kotlin에서는 Checked Exception을 강제하지 않아 코드에 따라 동작이 달라질 수 있습니다.
특히 Java-Kotlin 혼합 프로젝트에서는 UndeclaredThrowableException으로 변환될 수 있어 주의가 필요합니다.
트랜잭션을 장시간 유지하면 성능 저하가 발생할 수 있어, 트랜잭션을 분리하여 관리하는 전략이 필요합니다.
예를 들어, 카프카를 활용한 무손실 이벤트 처리에서는 트랜잭션을 짧게 유지하는 방식이 효과적입니다.
스프링의 @Transactional 어노테이션을 사용하다 보면 때로는 예상과 다르게 동작하는 경우를 마주치게 됩니다.
특히 예외 처리와 관련하여 롤백이 발생하지 않거나, 의도치 않게 발생하는 경우가 있어 주의가 필요합니다.
스프링에서 트랜잭션 롤백은 기본적으로 다음과 같이 동작합니다.
@Transactional
public void processOrder() {
// RuntimeException 발생 시 자동 롤백
throw new RuntimeException("주문 처리 실패");
// IOException은 롤백되지 않음
throw new IOException("파일 처리 실패");
}
@Service
class OrderService {
fun process() {
save() // @Transactional이 동작하지 않음
}
@Transactional
fun save() {
// 데이터 저장
}
}
위 코드에서 save() 메서드의 @Transactional은 동작하지 않습니다.
이는 스프링 AOP가 프록시 방식으로 동작하기 때문입니다.
@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)
}
}
}
}
이렇게 배치 크기를 제한하고 각각의 처리를 개별 트랜잭션으로 실행하면 메모리 사용량을 줄이고 성능을 개선할 수 있습니다.
트랜잭션의 롤백 동작을 정확히 이해하고 적용하는 것은 안정적인 애플리케이션 개발의 기본이라는것을 이번에 배우게 되었습니다.
특히 대용량 데이터를 다루는 경우, 트랜잭션 관리는 성능과 안정성에 직접적인 영향을 미치므로 신중한 설계가 필요합니다.
트랜잭션 관리는 백엔드 개발에서 가장 기본이 되는 부분임에도 불구하고, 실제로 다양한 상황에서 예상과 다르게 동작할 수 있다는 점을 다시 한번 깨달았습니다.
특히 Kotlin과 Java를 함께 사용하는 프로젝트에서는 예외 처리와 관련하여 더욱 신중한 접근이 필요하다는 것을 배웠습니다.
이번 포스팅을 통해 트랜잭션의 기본 개념부터 실무에서 마주칠 수 있는 다양한 케이스들을 정리하면서, 더 안정적인 애플리케이션을 만들기 위한 인사이트를 얻을 수 있었습니다.