ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Transaction Propagation과 예외 전파
    Spring 2023. 2. 12. 18:04

     

     

     

     

    이번 프로젝트를 진행하면서 try catch 예외처리 및 noRollbackFor 처리를 해준 부모 트랜잭션에서 계속해서 자식 트랜잭션의 예외 때문에 롤백되는 현상이 발생했는데,' 왜 롤백 하지 않도록 처리 했는데 예외가 발생했는가?'를 알아보면서 트랜잭션 전파와 예외에 대해서 공부하게 되었다. 알고보니 처리를 엉뚱한데에 해줘서 생긴 문제,,, 🤦🏻‍♀️

     

     

     

     

    공부를 얕게하면 안된다...

     

     

     

     

     

    트랜잭션 전파

    트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다.

     

     

     

     

    트랜잭션 전파 종류

    PROPAGATION_REQUIRED

    •  진행중인 트랜잭션이 없으면 새로 시작하고 이미 시작된 부모 트랜잭션이 있으면 이에 참여
    •  DefaultTransactionDefinition의 트랜잭션 전파 속성 (기본 전파 설정값)
    •  하나의 트랜잭션으로 묶여 부모나 자식에서 예외 발생시 모두 롤백
    •  부모에서 try-catch문을 작성하여도 이미 예외가 던져졌기 때문에 모두 롤백
    •  하나의 물리적 트랜잭션 안에 여러개의 논리적인 트랜잭션으로 구성된 경우 하나의 트랜잭션에서 unchecked exception이 발생하면 롤백마크를 하고 해당 물리적 트랜잭션이 전체 롤백됨

     

     

    PROPAGATION_SUPPORTS

    • 이미 시작된 부모 트랜잭션이 있다면 부모 트랜잭션을 사용하고 없으면 트랜잭션이 없이 동작

     

     

    PROPAGATION_NOT_SUPPORTED

    •  트랜잭션 없이 동작하도록 설정
    •  진행중인 트랜잭션이 있어도 무시
    •  특별한 메소드만 트랜잭션 적용에서 제외하고자 할 때 사용

     

     

    PROPAGATION_REQUIRES_NEW

    •  항상 새로운 트랜잭션을 시작
    •  독립적인 트랜잭션이 보장되어야 하는 코드에 적용
    •  nested 한 방식으로 호출되더라도 rollback이 각각 이루어짐
    •  자식에서 예외 발생시 부모까지 예외가 전파되어 try catch 해주지 않으면 모두 롤백됨
    •  부모 트랜잭션에서 예외가 발생하더라도 자식 트랜잭션은 롤백되지 않음
    •  자식 트랜잭션에서 예외가 발생하면 부모 트랜잭션까지 롤백이 전파되기때문에 롤백을 원하지 않는다면 try catch 문으로 예외 처리가 필요

     

     

    PROPAGATION_MANDATORY

    •  이미 시작된 부모 트랜잭션이 있다면 부모 트랜잭션을 사용하고 부모 트랜잭션이 없다면 예외를 던짐

     

     

    PROPAGATION_NEVER

    •  이미 시작된 부모 트랜잭션이 있다면 예외를 던짐

     

     

    PROPAGATION_NESTED

    •  이미 시작된 부모 트랜잭션이 있다면 save point로 지정해두고 비지니스로직 처리중 예외가 발생한다면 트랜잭션이 save point로 롤백하고 부모 트랜잭션이 없다면 새로운 트랜잭션을 생성하여 REQUIRED처럼 동작
    •  새로운 트랜잭션 생성이 아닌 트랜잭션 안에 트랜잭션을 생성
    •  부모 트랜잭션에서 예외 발생 시 부모, 자식 트랜잭션 모두 롤백
    •  자식 트랜잭션에서 예외 발생하고 부모 트랜잭션에서 예외가 없을 경우 자식 트랜잭션만 롤백
    •  부모가 실패하면 모두 실패하면서 자식의 실패가 부모에 영향을 주면 안될 때 사용 

     

     

     

    REQUIRED vs REQUIRED_NEW 예외 전파

    REQUIRED와 REQUIRED_NEW의 예외 전파를 살펴보자. REQUIRED에서는 하나의 물리적 트랜잭션으로 인식하여 자식에서 예외가 발생하면 모두 롤백된다는 것이 당연할 것이다. REQUIRED_NEW에서는 이와 달리 자식 트랜잭션에서 예외가 발생하더라도 별개의 트랜잭션으로 인식되어 부모의 트랜잭션이 롤백되지 않을 것 같지만, 사실은 그렇지 않다.

     

    다시 한번 정의를 살펴보자.

    • REQUIRED : 하나의 물리적 트랜잭션 안에 여러개의 논리적인 트랜잭션으로 구성
    • REQUIRED_NEW : 별도의 새로운 트랜잭션(커넥션)을 만듦

    단, 두 경우 모두 동일 스레드에서 진행 되기 때문에 REQUIRED_NEW에선 자식 트랜잭션에서 예외가 발생할 경우 자바가 콜스택을 하나씩 제거하면서 최초 호출한 곳까지 예외를 전파하기때문에 예외처리가 필요하다.

    REQUIRED의 경우 하나의 물리적 트랜잭션 안에 여러개의 논리적인 트랜잭션으로 구성된 경우이므로 하나의 트랜잭션에서 unchecked exception이 발생하면 롤백마크를 하고 롤백마크된 트랜잭션은 재사용이 불가능하기 때문에 해당 물리적 트랜잭션이 전체 롤백되기 때문에 예외처리 해놓았다 해도 트랜잭션은 이미 롤백될 것 이다.

     

     

     

     

     

     

    문제 발생 케이스

    문제가 발생한 케이스는 다음과 같았다.

    ParentService -> FirstChildService -> SecondChildService순으로 call을 하고 마지막 단계인 SecondChildService에서 예외가 발생한다. Transaction 전파 속성은 가장 기본인 REQUIRE를 사용하고 noRollbackFor설정은 가장 부모인 ParentService에만 선언해주었다. 각각의 클래스 모두에게 Transactional선언이 되어있다.

    @Service
    @Transactional(noRollbackFor=[RuntimeException::class])
    class ParentService(
        private val firstChildService: FirstChildService,
        private val repository: Repository
    ) {
    
        fun callFirstChild() {
            try {
                repository.save(Entity(name = "parentData"))
                firstChildService.callSecondChild()
            } catch (ex: RuntimeException) {
                ex.printStackTrace()
            }
        }
    }
    
    @Service
    @Transactional
    class FirstChildService(
        private val secondChildService: SecondChildService,
        private val repository: Repository
    ) {
        fun callSecondChild() {
            repository.save(Entity(name = "firstChildData"))
            secondChildService.doService()
        }
    }
    
    @Service
    @Transactional
    class SecondChildService(
        private val repository: Repository
    ) {
    
        fun doService() {
            repository.save(Entity(name = "secondChildData"))
            throw RuntimeException("예외를 발생시킵니다")
        }
    }

     

     

    위의 상태라면, 앞서 공부했듯이 당연히 위의 케이스는 REQUIRE 전파를 사용하므로 하나의 물리적 트랜잭션 안에서 여러 논리적인 트랜잭션을 소유하고 있지만, 한번 롤백 마크가 되면 모든 트랜잭션이 롤백된다. 가장 상위인 ParendService에 noRollbackFor설정이 되어있고 try-catch로 잡으려 한다고 하더라도 ParentService에 오기전에 예외가 발생하여 모든 트랜잭션이 롤백 된다.

    데이터베이스에 저장된 데이터가 없음

     

     

     

    이번엔 FirstChildService에도 noRollbackFor설정을 해주어보자.

    @Service
    @Transactional(noRollbackFor=[RuntimeException::class])
    class FirstChildService(
        private val secondChildService: SecondChildService,
        private val repository: Repository
    ) {
        fun callSecondChild() {
            repository.save(Entity(name = "firstChildData"))
            secondChildService.doService()
        }
    }

     

    당연히 여기서도 예외 발생 근원지인 SecondChildService에 noRollbackFor 설정이 없기 때문에 롤백된다.

     

    데이터 베이스에 저장된 데이터가 없음

     

     

     

     

    이제는 예외 발생 근원지인 SecondChildService에 noRollbackFor 설정을 해준다.

    @Service
    @Transactional(noRollbackFor=[RuntimeException::class])
    class SecondChildService(
        private val repository: Repository
    ) {
    
        fun doService() {
            repository.save(Entity(name = "secondChildData"))
            throw RuntimeException("예외를 발생시킵니다")
        }
    }

     

    여기서는 롤백 마크가 되지 않으면서 자바가 예외를 최초 호출 지점까지 전달하게 되므로 상위 레벨까지 예외가 전달되므로 예외는 발생하지만 데이터를 롤백하지 않는다.

    데이터가 잘 저장되었음

     

     

     

     

    우리는 ParentService에서 이미 try-catch문을 이용해서 전달된 예외처리를 해주고 있으므로 다음과 같이 noRollbackFor를 제거할 수 있다.

    @Service
    @Transactional
    class ParentService(
        private val firstChildService: FirstChildService,
        private val repository: Repository
    ) {
    
        fun callFirstChild() {
            try {
                repository.save(Entity(name = "parentData"))
                firstChildService.callSecondChild()
            } catch (ex: RuntimeException) {
                ex.printStackTrace()
            }
        }
    }

    데이터가 잘 저장되어 있음

     

     

    즉, 이번에 발생한 문제는 PROPAGATION_REQUIRE에서 noRollbackFor 설정을 엉뚱한곳에만 추가하고 예외를 발생시켜 모두 롤백되는 문제였다...! 전파 타입을 잘 확인하고 원하는 결과를 도출하기 위해서는 적절한 위치에 설정을 부여해주어야 한다‼️

     

     

     

     

    완성...! ✨

     

     

     

     

     

    ( 참고한 블로그 ✨)

    https://woodcock.tistory.com/40

     

    Transactional REQUIRES_NEW에 대한 오해

    서론 예전에 함께 스터디를 했던 스터디원이 트랜잭션에 관한 블로그 글을 공유하면서, 흥미로운 내용이라고 소개했다. 해당 글에서는 기존에 내가 알고있던 사실이 틀리다라고 얘기하는 내용

    woodcock.tistory.com

    https://techblog.woowahan.com/2606/

     

    응? 이게 왜 롤백되는거지? | 우아한형제들 기술블로그

    {{item.name}} 이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다.

    techblog.woowahan.com

    https://devlog-wjdrbs96.tistory.com/424

     

    [Spring] Transactional Propagation 정리하기

    @Transactional Propagation 알아보기 이번 글에서는 Spring Transactional 어노테이션에서 propagation 특징에 대해서 정리해보려 합니다. Propagation 옵션 설명 REQUIRED 기본 옵션 부모 트랜잭션이 존재한다면 부모

    devlog-wjdrbs96.tistory.com

    https://www.baeldung.com/spring-transactional-propagation-isolation

     

Designed by Tistory.