본문 바로가기

TIL

TIL) @Transactional Propagation, 스프링에서 초기화 코드를 넣기

1. @Transactional Propaction

스프링에서 트랜젝션을 설정하는 @Transactional 에는 전파레벨을 설정할 수 있다.

 

@Transactional(propagation = Propagation.<선택>)

 

  1. PROPAGATION_REQUIRED 
    1. 디폴트 값.
    2. 부모 트랜잭션이 있을 때 부모 트랜잭션에 참여. 없으면 새로운 트랜잭션 시작.
    3. 자식 트랜잭션에서 예외가 발생하면 부모 트랜잭션까지 모두 롤백.
  2. PROPAGATION_SUPPORTS
    1. 부모 트랜잭션이 있을 때 부모 트랜잭션에 참여. but 없으면 트랜잭션 없이 동작.
  3. PROPAGATION_MANDATORY
    1. 부모 트랜잭션이 있을 때 부모 트랜잭션에 참여. 부모 트랜잭션이 없다면 Exception 발생.
  4. PROPAGATION_REQUIRES_NEW
    1. 부모 트랜잭션에 상관없이 무조건 새로운 트랜잭션 실행. 부모 트랜잭션이 있다면 자식 트랜잭션 메서드 전에 부모 트랜잭션을 suspend 시키고, 자식 트랜잭션 메서드가 끝날 때 까지 부모 트랜잭션을 잠시 대기시킨 후 자식 트랜젝션이 끝나면 부모 트랜잭션을 다시 resume 한다.
    2. 새로운 트랜잭션이기 때문에, 자식 트랜잭션의 롤백은 부모 트랜잭션에 전파되지 않는다.
  5. PROPAGATION_NOT_SUPPORTED
    1. 부모 트랜잭션에 상관없이 무조건 트랜잭션 없이 동작. 부모 트랜잭션이 있다면 이 트랜잭션(non-transcation) 메서드를 시작하기 전 부모 트랜잭션을 suspend 시키고, 끝난 후 다시 resume 한다.
  6. PROPAGATION_NEVER
    1. 부모 트랜잭션에 상관없이 무조건 트랜잭션 없이 동작. 부모 트랜잭션이 존재하면 Exception 발생.
  7. PROPAGATION_NESTED
    1. 자식 트랜잭션에서 롤백이 발생했을 때, 부모 트랜잭션이 자식 트랜잭션에서 잡아둔 savepoint 까지 롤백한다. (savepoint 를 지원하는 DB 에서만 동작한다.)
    2. 부모 트랜잭션이 없으면 REQUIRED 와 같다.
    3. 부모 트랜잭션이 있다면 중첩 트랜잭션으로 자식 트랜잭션을 새로 생성한다. 위와 다른 점은 중첩으로, 독립적으로 실행한다는 것. 즉 위와는 다르게, 부모 트랜잭션을 suspend, resume 하지 않는다.
    4. JtaTransactionManager 로 트랜잭션 관리를 할 때에는 안되고, DataSourceTransactionManager 를 직접 사용할 때에만 동작한다. 아니면 아래와 같은 에러 발생. (SAVEPOINT 가 있어야 하기 때문에 JPA 단에서 추상화해서 처리할 수 없기 때문이 아닐까.)
JpaDialect does not support savepoints - check your JPA provider's capabilities

 

 

 

트랜잭션 전파레벨에 대한 글

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

 

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

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

techblog.woowahan.com

 

1. 부모, 자식 트랜잭션이 한 트랜잭션에서 일어났으면 좋겠고, (REQUIRED) (PROPAGATION_REQUIRES_NEW 는 완전히 독립적임)

2. 자식 트랜잭션 메서드에서 발생한 예외를 바로 처리하지 않고 (롤백 발생)

3. 부모 트랜잭션 메서드에서 예외처리를 적절하게 한다면

 

부모 트랜잭션은 커밋될까 ? -> NO

 

@Transcation 에서는 PROPAGATION_REQURED 로 하나의 트랜잭션으로 동작한다 하더라도, 자식 트랜잭션이 끝날 때 마다 완료처리를 한다.

@Transcation 에서 Exception 이 발생하면, [rollback-only] 마킹을 한다.

이 마킹은, 부모 트랜잭션에서 예외를 잡아 처리했다 하더라도, 부모 트랜잭션 메서드를 최종 커밋할때 마킹 떄문에 롤백하게 된다.

 

=> 자식 트랜잭션중에 하나라도 Exception 이 발생하면 전체 트랜잭션이 롤백된다.

 

globalRollbackOnParticipationFailure 주석

https://github.com/spring-projects/spring-framework/blob/4560dc2818ae1d5e1bc5ceef89f1b6870700eb1f/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L265

 

GitHub - spring-projects/spring-framework: Spring Framework

Spring Framework. Contribute to spring-projects/spring-framework development by creating an account on GitHub.

github.com

 

 

결국 내부 트랜잭션의 결과가 외부 트랜잭션의 결과에 영향을 끼치지 않게 하는 방법이 무엇이 있을지 생각해보자.

  1. PROPAGATION_REQUIRES_NEW 사용 : 독립적인 트랜잭션을 만들 수 있지만, 부모 트랜잭션은 일시정지된다.
  2. PROPAGATION_NESTED 사용 : 중첩 트랜잭션으로 독립적인 트랜잭션을 만들 수 있지만, DataSourceTransactionManager 를 직접 사용해야 한다.
  3. AbstractPlatformTransactionManager.globalRollbackOnParticipationFailure 디폴트 값이 true 인데 false 로 바꾸기 : 이러면 자식 트랜잭션이 예외로 롤백이 되어도 부모 트랜잭션은 계속 진행할지 말지 결정할 수 있다. 하지만, 데이터 액세스 실패 후에도 모든 참여 리소스가 트랜잭션 커밋을 계속할 수 있음이 보장되어야 한다. (솔직히 무슨말인지 잘 모르겠다.)

 

 

 

2 스프링 부트 앱에 초기화 코드를 넣는 방법

스프링 부트 시작하자 마자 무언가를 하고 싶다.

그런데 생성된 빈들을 사용하고 싶다.

 

방법은 세가지가 있다.

 

1. commandLineRunner

@Bean
fun myRunner(testController: TestController): CommandLineRunner {
    return CommandLineRunner { println("test : $testController") }
}


또는

@Component
class MyRunner : CommandLineRunner {

    override fun run(vararg args: String?) {
    //TODO
    }
}

 

2. ApplicationRunner

@Bean
fun myRunner(testController: TestController): ApplicationRunner {
    return ApplicationRunner { println("test : $testController") }
}

또는

class Test: ApplicationRunner {

    override fun run(args: ApplicationArguments?) {
        TODO("Not yet implemented")
    }
}

 

 

3. @EventListener(ApplicationReadyEvent::class)

@EventListener(ApplicationReadyEvent::class)
fun init() {
    println("go")
}

 

스프링 부트가 스프링 컨텍스트를 띄우면서 발생하는 이벤트를 잡아서 처리하는 방식이다.

 

스프링 부트가 컨텍스트를 띄울 때 발생시키는 이벤트의 종류에는 SpringApplicationEvent 클래스를 상속한 7가지 이벤트가 있다.

 

그리고, 그 컨텍스트를 띄우는 코드는 아래 메서드이고, 안을 보면 이벤트를 퍼블리시하는 과정을 볼 수 있다.

SpringApplication.ConfigurableApplicationContext

 

안을 잘 살펴보면,

ApplicationStartedEvent -> ApplicationRunner -> CommandLineRunner -> ApplicationReadyEvent

순으로 실행됨을 알 수 있다.

 

위 순서로 쓴 대로 부트에 추가되었으며, 가장 최근인 @EventListener 방식을 주로 쓰기로 하고,

스프링 나름의 준비가 모두 된 후 ApplicationReadyEvent 에서 내가 돌리고 싶은 초기화 코드를 쓰는게 좋아 보인다.

 

 

https://www.youtube.com/watch?v=f017PD5BIEc