본문 바로가기
Programming/Languages (Java, etc)

[SPRING] @Transactional을 얼마나 이해했는지 보자

by kghworks 2023. 1. 4.

목차

  • @Transcational
  • CASE
  • 정리
  • 참고

 

2022.08.19 - [개발/데이터베이스 시스템] - 트랜잭션 (transaction)

 

트랜잭션 (transaction)

목차 트랜잭션이란 트랜잭션 제어 문장 원자성 영속성 트랜잭션에서 무결성 제약조건 검사 나쁜 트랜잭션 습관 분산 트랜잭션 자율 트랜잭션 참고  본 포스팅은 도서 "전문가를 위한 오라클 데

kghworks.tistory.com

 

 이전 포스팅에서 자동커밋을 사용하는 나쁜 트랜잭션 습관을 지양하라고 하였습니다. 거듭 설명하지만 트랜잭션은 논리적인 업무 단위이며 디비가 하나의 상태에서 다음 상태로 바꾸는 작업 단위이기 때문에 적절한 시점에 커밋을 해야 합니다. (DML 중간중간에 자주 커밋하는 것이 아니고)

 

 이번 포스팅에서는 스프링 프레임워크의 @Transcational을 알아보고 이 프레임워크에서 어떻게 트랜잭션을 위와 같이 다룰 수 있는지 확인합니다.

 

 

스프링은 CheckedExcpetion을 기본적으로 롤백한다.

라고 이해하지 마시고, 내가 충분히 옵션 값으로 그 transaction에 대해 제어할 수 있습니다. 이번 포스팅에서 소개할 케이스들을 통해 스프링에서 db transcation을 좀 더 편리하게 다룰 수 있기를 바랍니다.


@Transcational

 JDBC 드라이버를 통해 접속하는 스프링은 auto-commit 이 default입니다. db-side에서는 트랜잭션의 커밋 시점이 비즈니스의 종료시점이어야 명확한 비즈니스인데, 스프링은 정확히 반대되는 표준을 가지고 있는 겁니다.

 

 스프링에서는 이에 대한 조치로 많이들 @Transactional을 사용하는데요. 선언적 트랜잭션 이라고도 합니다. 뭐 아무튼 @Transcaional을 사용함으로써 해당 scope에 프록시를 만들어 비즈니스 로직을 수행한다는 건데요. 케이스 별로 확인하면서 어떻게 쓰는 것이 좋을지 알아봅니다.


Case

case 1 : RuntimeException 발동

public void updt01() {
    // LECTURE 이름 변경
    transactionMapper.updt();

    // RuntimeException 발동!
    throw new RuntimeException("new Exception!!");
}

...

 <update id="updt">
    UPDATE university.pr_lecture
    SET NAME = '컴퓨터공학',  DT_MOD = SYSDATE()
    WHERE NAME = '공학'
</update>

exception 발생 로그
실행결과

 updt01()이 실행되는 동안 Runtime Exception이 발동했음에도 트랜잭션은 정상 반영 되었습니다. 이는 심각한 비즈니스 오류가 될 수 있습니다. updt01에서 발생할 수 있는 여러 예외들에도 불구하고 트랜잭션은 commit 되기 때문입니다. 

 

case 2 : RuntimeException 발동 (@Transactional 사용)

// @Transactional을 사용하여 updt01을 하나의 트랜잭션으로 수행
@Transactional
public void updt02() {
    // LECTURE 이름 변경
    transactionMapper.updt();

    // RuntimeException 발동!
    throw new RuntimeException("new Exception!!");
}

exception 발생 로그
실행결과

@Transcational 이 적용되어 RuntimeException에 반응하여 트랜잭션을 rollback 했습니다. 

 

case 3 : CheckedException 발동 (@Transactional 사용)

@Transactional
public void updt03() throws SQLException {
    // LECTURE 이름 변경
    transactionMapper.updt();

    // CheckedException > SQLException 발동!
    throw new SQLException();
}

CheckedException 발생 로그
실행결과

 

@Transcational에도 불구하고 트랜잭션이 commit 되었습니다. case 2~3에서 알 수 있듯 @Transcational은 UncheckedException (RuntimeException)에 대해서 rollback 하는 것이 default라는 것을 알 수 있습니다. 그러나 개발자가 설정하지 않으면 적용될 기본값일 뿐, 우린 많은 옵션을 줄 수 있습니다. 

 

case 4 : rollbackFor 속성

@Transactional (rollbackFor = SQLException.class) //SQLException에 대해 rollback
public void updt04() throws SQLException {
    // LECTURE 이름 변경
    transactionMapper.updt();

    // CheckedException > SQLException 발동!
    throw new SQLException();
}

 

실행결과

 

 트랜잭션이 성공적으로 rollback 되었습니다. rollbackFor 속성을 사용해 CheckedException에도 불구 트랜잭션을 rolblack 할 예외를 지정해줌으로써 문제를 해결했습니다. 만일 Exception.class로 속성값을 지정한다면 updt04()에서 발생하는 모든 예외에 대해 rollback이 가능합니다.

 

case 5 : try - catch

@Transactional
    public void updt05() {

        try{
            // LECTURE 이름 변경
            transactionMapper.updt();

            // RuntimeException 발동!
            throw new RuntimeException("new Exception!!");
        }catch (Exception e){
            /*
            * handle the Exception
            * ....
            *
            * */
        }

    }

 

실행결과

 예외를 catch 하여 처리했기 때문에 트랜잭션은 성공적으로 commit 되어버립니다. 

 

case 6 : nested transaction (중첩 트랜잭션)

@Transactional
public void updt06() {
    // LECTURE 이름 변경
    transactionMapper.updt();

    try{
        transactionServiceTwo.updt06a();
    }catch (RuntimeException e){
        e.printStackTrace();
        /*
         * handle the Exception
         * ....
         * */
    }
}


// TransactionServiceTwo.java

@Transactional
public void updt06a() {
    // LECTURE 이름변경 2
    transactionMapper.updtA();

    throw new RuntimeException();
}

...

<update id="updtA">
    UPDATE university.pr_lecture
    SET NAME = '공기역학',  DT_MOD = SYSDATE()
    WHERE NAME = '역학'
</update>

exception 발생
실행결과

  updt06a에서 발생한 예외를 handle 했음에도 updt06까지 모두 rollback 되었습니다. @Transactional 은 propagation 속성을 가집니다. Propagation은 트랜잭션의 boundary를 결정하고, Propagation.REQUIRED 가 default입니다.Propagation.REQUIRED 는 기존에 닫히지 않은 트랜잭션이 있다면 해당 트랜잭션을 사용하겠다는 것입니다. 아래 로그를 보시면 실제 새로운 트랜잭션을 열지 않고 updt06()의 트랜잭션에 참여한 것을 확인할 수 있습니다.

Participating in existing transaction 로그

Creating new transaction with name [com.kghdev.transaction.TransactionService.updt06]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
...
Participating in existing transaction

 따라서 두 비즈니스는 한 트랜잭션에서 수행되었고, RuntimeException이 발동했으므로 모두 rollback 되었습니다.

 

case 6 : nested transaction, Propagation.REQUIRES_NEW

@Transactional
public void updt06() {
    // LECTURE 이름 변경
    transactionMapper.updt();

    try{
        transactionServiceTwo.updt06b();
    }catch (RuntimeException e){
        e.printStackTrace();
        /*
         * handle the Exception
         * ....
         * */
    }
}

// TransactionServiceTwo.java

@Transactional (propagation = Propagation.REQUIRES_NEW)
public void updt06b() {
    // LECTURE 이름변경 2
    transactionMapper.updtA();

    throw new RuntimeException();
}

실행결과

 

 실행 결과 두 번째 트랜잭션만 성공적으로 rollback 되었고, 첫 번째 트랜잭션은 commit 되었습니다. Propagation.REQUIRES_NEW 속성값을 이용하여 updt06b()에 대하여 새로운 트랜잭션을 열도록 지정해 주었기 때문입니다. 아래 로그를 통해 각각 독립적인 트랜잭션을 사용했음을 알 수 있습니다. 

 

로그

Creating new transaction with name [com.kghdev.transaction.TransactionService.updt06]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
...
Suspending current transaction, creating new transaction with name [com.kghdev.transaction.TransactionServiceTwo.updt06b]

 

case 7 : nested transaction, Propagation.REQUIRES_NEW, no try - catch

@Transactional
public void updt07() {
    transactionMapper.updt();
    transactionServiceTwo.updt07a();
}

// TransactionServiceTwo.java

@Transactional (propagation = Propagation.REQUIRES_NEW)
public void updt07a() {
    transactionMapper.updtA();

    throw new RuntimeException();
}

실행결과

 

 updt07a()에서 발생한 예외가 updt07() 스코프까지 전이되었습니다. 둘 다 rollback 되었습니다. 새로운 트랜잭션을 사용했지만 updt07은 해당 RuntimeException을 감지했고, handle 하지 않았기 때문입니다.

 

case 8 : nested transaction, Propagation.REQUIRES_NEW,  outer transcation Exception

@Transactional
public void updt08() {
    transactionMapper.updt();
    transactionServiceTwo.updt08a();

    throw new RuntimeException("new outer Exception!");
}

//TransactionServiceTwo.java

@Transactional (propagation = Propagation.REQUIRES_NEW)
public void updt08a() {
    transactionMapper.updtA();
}

 

실행결과

 

 바깥 트랜잭션이 rolblack 되었지만 자식 트랜잭션은 성공적으로 commit 되었습니다. Propagation.REQUIRES_NEW 덕분입니다.

 


정리

 

 @Transactional에 대해 알아봤습니다. AbstractPlatformTransactionManager, RuntimeException, CheckedException 등 기초적으로 알고 계셨다면 위 case들을 충분히 따라가실 수 있을 거라고 생각됩니다. 다만, 위 case에서 이해되지 않거나 모르는 부분들이 있었다면, spring과 db 양쪽에서 예외, 트랜잭션에 대해 다소 깊게 이해하고 가시기를 추천드립니다. Transcation을 스프링에서 잘 다룰 수 있게 되었을 때 우리가 예측가능한 범위 내에서 Exception을 대비해 좋은 비즈니스를 설계할 수 있습니다.

 

 추가로 propagation의 값으로 Propagation.REQUIRES_NEW만 소개했는데 다른 값들도 알아보시면 좋겠습니다.

 


참고

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html

 

Transactional (Spring Framework 5.3.23 API)

Defines zero (0) or more exception Classes, which must be subclasses of Throwable, indicating which exception types must not cause a transaction rollback. This is the preferred way to construct a rollback rule (in contrast to noRollbackForClassName()), mat

docs.spring.io

 

https://medium.com/geekculture/spring-transactional-rollback-handling-741fcad043c6

 

Spring @Transactional Rollback Handling

By default, the spring boot transaction is auto-commit. Every single SQL statement is in its own transaction and will commit after…

medium.com

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

 

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

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

techblog.woowahan.com

 

댓글