3 분 소요

@Transactional

트랜잭션 내에서 이뤄지는 모든 작업은 완료 시점에 모두 반영되거나, 모두 반영되지 않아야 한다. (atomic)
그래서 Spring에서는 @Transactional을 선언함으로써 특정 작업을 트랜잭션 범위 안에 넣을지 여부를 결정하게 된다.
트랜잭션을 시작할 때 아래 과정을 거치게 된다.

트랜잭션 매니저에 트랜잭션 요청 → 데아터 소스로 커넥션 요청 → 생성된 커넥션에서 수동 커밋 모드로 설정 
→ 트랜잭션 동기화 매니저에 커넥션 저장
  • 기존 트랜잭션에 참여하려는 요청이 있을 때, 트랜잭션 동기화 매니저에 저장된 커넥션에 접근해 커넥션을 꺼내올 수 있음

Propagation.REQUIRED

@Transactional의 기본 전파 옵션은 REQUIRED여서, 내부에서 호출한 메소드가 @Transactional이 붙어있다면 트랜잭션 동기 화 매니저에 접근해 커넥션을 가져와 기존 트랜잭션에 참여하게 된다. 외부에서 트랜잭션을 시작한 쪽을 물리 트랜잭션, 내부에서 기존 트랜잭션에 참여하는 쪽을 논리 트랜잭션이라고 할 수 있겠다. 논리 트랜잭션은 트랜잭션을 시작, 커밋하지 않는다. 시작, 커밋하는 쪽 은 항상 물리 트랜잭션에서 이뤄진다.

public @interface Transactional {
    Propagation propagation() default Propagation.REQUIRED;
    ...
}
?


트랜잭션이 필요한 여러 작업이 있는데 이를 모두 하나의 트랜잭션으로 묶고 싶을 때 전파 옵션을 REQUIRED로 설정하면 된다!
당연한거지만 하나의 트랜잭션에서 처리하므로 어떤 작업에서 Unchecked Exception이 발생했을 때 모든 작업을 반영하지 않도록 롤백을 수행하게 된다.

Propagation.REQUIRES_NEW

위처럼 하나의 트랜잭션이 아니라, 분리해서 처리하고 싶을 때가 있다! 이 때는 전파 옵션을 REQUIRES_NEW로 설정해주면 된다.
만약 외부에서 트랜잭션을 시작하고(물리 트랜잭션), 내부에서 REQUIRES_NEW를 설정했다면 기존 트랜잭션에 참여하지 않고 새로운 트랜잭션을 시작하게 된다. 그러면 논리 트랜잭션이 아니라 물리 트랜잭션을 수행하는 셈이다. 커밋도 수행할 수 있게된다!

?


롤백(Rollback)

Propagation.REQUIRED

논리 트랜잭션에서 Unchecked Exception이 발생했을 때 롤백을 수행한다고 언급했었다. 모두 같은 트랜잭션에 속해있기 때문에 내부에서 던지는 예외가 외부까지 도달해 작업한 내용을 모두 롤백하는 상황은 쉽게 상상해볼 수 있다.
(참고로 Checked Exception은 기본적으로 롤백을 수행하지 않지만, 트랜잭션에 rollbackFor 옵션을 사용하면 특정 예외를 롤백하도록 설정해줄 수 있다)

만약 외부에서 내부가 던지는 Unchecked Exception(이하 예외)을 try-catch로 묶어서 처리해준다면 더 이상 예외 전파가 이뤄지지 않는거니까 롤백이 안되지 않을까? 라고 생각할 수 있다. 하지만 내부의 논리 트랜잭션에서는 예외 발생 시점에서 트랜잭션 동기화 매니저에 rollback-only=true로 설정해놓기 때문에 이후 커밋 시점에서 전파된 예외가 try-catch로 처리가 됐든 상관없이 롤백을 수행한다.

?


코드로 살펴보면, TxTestService.firstCall() -> TxTestService2.secondCall() 흐름으로 이어진다.
secondCall()에서 @Transactional만 선언했으므로 firstCall()에서 생성한 물리 트랜잭션에 참여하게 된다.
각각 롤백이 됐는지 여부를 확인하기 위해 firstCall(), secondCall()에서 save()를 호출하도록 했다.

// TxTestService
@Transactional
fun firstCall() {
    testRepository.save(TestEntity("rollbackTestEntity"))

    try {
        txTestService2.secondCall()
    } catch (e: RuntimeException) {
        log.warn("예외 처리 완료", e)
    }
}
// TxTestService2
@Transactional
fun secondCall() {
    testRepository.save(TestEntity("rollbackTestEntity2"))
    throw RuntimeException()
}


firstCall()을 호출했을 때 insert 쿼리가 2번 나가고 secondCall()에서 발생한 예외로 처리하는 로그를 확인할 수 있지만

Hibernate: 
    insert 
    into
        test_entity
        (name, id) 
    values
        (?, default)
Hibernate: 
    insert 
    into
        test_entity
        (name, id) 
    values
        (?, default)
2024-04-10T17:22:35.136+09:00  WARN 83528 --- [p-nio-80-exec-1] c.e.demo.test.service.TxTestService      : 예외 처리 완료


이후 로그에서 secondCall()에서 설정한 rollback-only때문에 firstCall()에서 커밋하는 시점에 전체 롤백을 수행한다.

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
...

실제로 DB에도 insert한 내용들이 아무 것도 반영되지 않는다.
?

Propagation.REQUIRES_NEW

전파 옵션을 REQUIRES_NEW로 설정해 내부 트랜잭션을 새로 시작해 외부와 다른 트랜잭션을 수행한다면, 서로 다른 물리 트랜잭션이므로 내부에서 예외 발생 시 rollback-only=true를 설정하는 작업없이 바로 롤백을 수행한다. 그니까 커밋 or 롤백하는 공간이 분리된 것이다.

그치만 내부에서 던지는 예외는 외부에도 전파가 된다는 건 여전하기 때문에, 당연히 이걸 try-catch로 처리해주지 않으면 롤백된다. try-catch로 처리해준다면, 외부 트랜잭션은 정상적으로 커밋되고 내부 트랜잭션만 롤백된다!

?


코드로 살펴보자. 위의 테스트 코드에서 secondCall() 전파 옵션을 REQUIRES_NEW로 설정해준 것만 다르다.

// TxTestService
@Transactional
fun firstCall() {
    testRepository.save(TestEntity("rollbackTestEntity"))

    try {
        txTestService2.secondCall()
    } catch (e: RuntimeException) {
        log.warn("예외 처리 완료", e)
    }
}
// TxTestService2
@Transactional(propagation = Propagation.REQUIRES_NEW) // 전파 옵션 수정
fun secondCall() {
    testRepository.save(TestEntity("rollbackTestEntity2"))
    throw RuntimeException()
}

firstCall() 호출 시 insert 쿼리가 2번 날아가지만, DB에 최종 저장된 내용은 firstCall()에서 저장한 녀석뿐이다.
secondCall()에서 insert한 녀석은 롤백돼서 DB에 저장되지 않는다.

Hibernate: 
    insert 
    into
        test_entity
        (name, id) 
    values
        (?, default)
Hibernate: 
    insert 
    into
        test_entity
        (name, id) 
    values
        (?, default)
2024-04-10T17:15:31.138+09:00  WARN 82955 --- [p-nio-80-exec-1] c.e.demo.test.service.TxTestService      : 예외 처리 완료


?

References

카테고리:

업데이트:

댓글남기기