Skip to content

Latest commit

 

History

History
164 lines (129 loc) · 7.71 KB

동일한 Bean(Class)에서 @Transactional 동작 방식.md

File metadata and controls

164 lines (129 loc) · 7.71 KB

개요

동일한 빈(클래스)에서 @Transactional을 사용할 때 발생할 수 있는 문제점
- AOP의 특성을 고려하라

개선 정리(올바른 정리)


↑
↑
↑
↑
↑
↑
↑


보다 구체적이고 상세한 예시들을 직접 테스트해보며
기존의 기록을 다시 업데이트
아래 기록은 혹여나 잘못 생각했던 부분들을 남겨둬서 추후에 다시 생각해볼 수 있도록..

문제 상황

  • 스크린샷 2022-07-03 오후 5 36 02
  • 스크린샷 2022-07-03 오후 5 34 07
1.현재 컨트롤러에서 service.something()을 호출
2. something()은 동일 클래스에 존재하는 save()를 호출

위 사진에서처럼 service.save()에서 exception이 발생했을 때 모두 롤백이 될까?
  • 스크린샷 2022-07-03 오후 5 37 19
Input으로 10을 넘겼을 때 9에서 예외가 발생할 것
⭐️그런데 1~8까진 모두 rollback되지 않았다??!!!!!
즉 내가 기대한것은 AOP를 활용한 @Transactional을 통해 something()매서드를 트랜잭션이라는 부가기능으로 감싸서 동작하는것을 기대
 == > something() 매서드는 하나의 트랜잭션으로 수행되어 all or nothing - 하나라도 실패하는 경우 모두 롤백될것을 기대했다
 
그러나 여기 결과를 고려했을 때 something()은 단일 트랜잭션으로 동작하지 않았으면
repo.save()마다 단일 트랜잭션을 수행한 것
즉 Bean에 대해서 AOP가 동작하지 않았다고 판단할 수 있다.

무엇이 문제일까? - 의심

눈에띄는것은 바로 something()에서 내부 매서드를 호출
즉 외부에서 바로 service.save()에 접근한것이 아니다

원인

일단 스프링부트는 AOP Proxy 생성 방법으로 CGLIB을 기본 채택

그리고 Proxy Default Mode에서는 외부 매서드(즉 외부bean / 서로 다른 bean)에서 호출하는 경우에만 프록시가 동작
즉 동일빈내에서는 this로 매서드를 호출하고 이 경우 프록시 객체가 아닌 원본 객체의 매서드를 실행

결과적으로 지금 나의 코드는 다른 bean이 아닌 // myService에서 동일한 bean(myService) 내부 매서드를 호출하는 형태고
그렇기에 프록시가 동작하지 않았고  // 프록시가 동작하지 않기에 AOP를 통한 @Transactional이 동작하지 않은것이다

해결 방법

그렇다면?? 
해결은 어떻게??

바로 bean을 분리하는것
즉 myService에서 동일 bean 내부 매서드를 호출하지 않고
save() 매서드를 별도의 bean으로 빼고 이를 호출하는 형태를 가져가면 된다.
  • 스크린샷 2022-07-03 오후 5 55 50
  • 스크린샷 2022-07-03 오후 5 55 57
  • 스크린샷 2022-07-03 오후 5 56 01
  • 스크린샷 2022-07-03 오후 5 55 39

  • 그런데 다음 상황도 한 번 예측해보자
  • 스크린샷 2022-10-06 오후 7 37 12
전파레벨이 new라면 새로운 트랜잭션이 동작하고 서로 영향을 받지 않기에 1~number-1까진 저장이되어야할텐데...
모두 롤백처리된다. 즉 new 옵션이 작동하지 않는것
왜냐하면!!!

결국에 something() 매서드에서 맨 마직에 호출하는 매서드인 save()는 사실 this.save()이고 
이 경우 프록시 객체의 매서드가아닌 원본 객체의 매서드를 실행하기때문에 호출당하는쪽의 매서드의 @Transaction 어노테이션은 동작하지 않는다.
따라서 이 경우도 빈을 분리하는 방법을 통해 해결할 수 있다.
그런데 depth가 깊어지면?
빈을 계속 분리할 수 있나?

정확히 알고가자

  • 스크린샷 2022-10-19 오후 4 47 56
외부에서 호출되는 매서드가 first()라고 했을때
second()의 @Transactional은 동작할까??


안한다. 이것도 결국 this.second()로 
서로다른 빈에서 호출하는게 아니라 동일빈 / this에 의해 호출되고
프록시가 아닌 원본 매서드가 활용되는것

그런데!!!! 
second()의 save()매서드의 동작 자체는 first()에서 시작한 트랜잭션에 묶여 
하나의 트랜잭션으로 처리된다

왜!!!!냐면
바로 jpaRepository가 상속받은 crudRepository의 구현체이 simpleJpaRepository를 살펴보면 알 수 있듯이
crudRepository의 매서드들은 기본적으로 @Transactionl이 붙어있다


즉 결과적으로 second()의 save()매서드는 서로다른빈에서 simpleJpaRepository빈의 save()매서드를 호출하는거기에
@Transactional이 동작하고 기본 required로 이미 시작된 트랜잭션이 전파된다
  • 스크린샷 2022-10-19 오후 4 50 01

해결 방법2 - dependency lookup

  • 스크린샷 2022-11-07 오후 7 28 13
application context를 의존성으로 갖고 
현재 자기자신 빈을 가져오면
  • 스크린샷 2022-11-07 오후 7 32 21
가져온 빈인 mock을 봐보자
this.save()의 경우 원본 매서드가 호출되지만
mock은 cglib에 의해 생성된 프록시 객체고 그렇기때문에 mock.save()의 transactional이 동작한다.
실제로 전파레벨 new도 동작하는데
  • 스크린샷 2022-11-07 오후 7 26 38
근데 왜 mock 변수는 프록시 객체일까?
- @transactional 즉 aop가 동작해야하는 빈 대상으론 프록시 객체가 생성되고 
- 빈 레지스트리에 해당 프록시 객체가 등록된다.

그래서 빈으로 등록될 객체에 aop 적용대상이 하나도 없으면
여기선 transactional 어노테이션이 하나도 없으면 원본 객체타입이다. mock이
근데 application context라는 거대한 스프링코드를 의존성으로 갖으면
테스트가 가능할까? 이 부분은 고려해봐야한다.