ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Transactional 사용 시 주의사항
    카테고리 없음 2024. 6. 27. 14:20
    • @Transactional은 Spring AOP 기반이며, Spring AOP는 Proxy 기반으로 동작함
    • @Transactional 메소드가 호출되면, Proxy 객체를 생성하고 트랜잭션 생성/커밋/롤백 등의 작업을 proxy 객체에게 위임함
    • Proxy 객체가 호출될 때, 이 메소드를 가로채서 부가 기능들을 proxy 객체에게 위임함

    참고) Spring AOP proxy mechanisms (JDK dynamic and CGLIB) - TedBlob - Technical Posts

     

    Spring AOP proxy mechanisms (JDK dynamic and CGLIB) - TedBlob - Technical Posts

    How would Spring AOP intercept the call to the target method and execute the code inside advice? It uses proxying mechanisms.

    tedblob.com

     

    • Spring AOP 기반 기능들을 사용 시, Self Invocation 문제로 인해 장애가 발생할 수 있음

    • caller: proxy.bar() 호출
    • proxy 객체는 call을 가로채 Advice methods 실행
    • proxy 객체는 SimplePojo.bar()를 호출하므로 SimplePojo 객체에  대한 모든 호출은 proxy 객체만 통과함

    • @Transactional을 foo()에만 선언하고, 외부에서 bar()를 호출하고, bar()이 foo()를 호출하는 경우에는?
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class PharmacyRepositoryService {
        private final PharmacyRepository pharmacyRepository;
    
        // self invocation test
        public void bar(List<Pharmacy> pharmacyList) {
            log.info("bar CurrentTransactionName: "+ TransactionSynchronizationManager.getCurrentTransactionName());
            foo(pharmacyList);
        }
    
        // self invocation test
        @Transactional
        public void foo(List<Pharmacy> pharmacyList) {
            log.info("foo CurrentTransactionName: "+ TransactionSynchronizationManager.getCurrentTransactionName());
            pharmacyList.forEach(pharmacy -> {
                pharmacyRepository.save(pharmacy);
                throw new RuntimeException("error"); // 예외 발생
            });
        }
    
    
        // read only test
        @Transactional(readOnly = true)
        public void startReadOnlyMethod(Long id) {
            pharmacyRepository.findById(id).ifPresent(pharmacy ->
                    pharmacy.changePharmacyAddress("서울 특별시 광진구"));
        }
    
        @Transactional(readOnly = true)
        public List<Pharmacy> findAll() {
            return pharmacyRepository.findAll();
        }
    }
    • foo(): transactional이 정상적으로 실행되는지를 확인하기 위해 save() 함수 실행 후 RuntimeException을 발생시킴
    • save()가 호출되면 pharmacyRepository에는 pharmacy가 저장됨
    • 스프링의 Transactional 어노테이션은, RuntimeException이 발생하면 롤백하도록 되어있음
    • 따라서 Transactional이 정상적으로 실행된다면 save()는 롤백되어 실행 취소가 될 것으로 예상함
    테스트 코드
    class PharmacyRepositoryServiceTest extends AbstractIntegrationContainerBaseTest {
    
        @Autowired
        private PharmacyRepositoryService pharmacyRepositoryService
    
        @Autowired
        private PharmacyRepository pharmacyRepository
    
        def setup() {
            pharmacyRepository.deleteAll()
        }
    
        def "self invocation"() {
    
            given:
            String address = "서울 특별시 성북구 종암동"
            String name = "은혜 약국"
            double latitude = 36.11
            double longitude = 128.11
    
            def pharmacy = Pharmacy.builder()
                    .pharmacyAddress(address)
                    .pharmacyName(name)
                    .latitude(latitude)
                    .longitude(longitude)
                    .build()
    
            when:
            pharmacyRepositoryService.bar(Arrays.asList(pharmacy))
    
            then:
            def e = thrown(RuntimeException.class)
            def result = pharmacyRepositoryService.findAll()
            result.size() == 1 // 트랜잭션이 적용되지 않는다( 롤백 적용 X )
        }
    
        def "transactional readOnly test - 읽기 전용일 경우 dirty checking 반영 되지 않는다. "() {
    
            given:
            String inputAddress = "서울 특별시 성북구"
            String modifiedAddress = "서울 특별시 광진구"
            String name = "은혜 약국"
            double latitude = 36.11
            double longitude = 128.11
    
            def input = Pharmacy.builder()
                    .pharmacyAddress(inputAddress)
                    .pharmacyName(name)
                    .latitude(latitude)
                    .longitude(longitude)
                    .build()
    
            when:
            def pharmacy = pharmacyRepository.save(input)
            pharmacyRepositoryService.startReadOnlyMethod(pharmacy.id)
    
            then:
            def result = pharmacyRepositoryService.findAll()
            result.get(0).getPharmacyAddress() == inputAddress
        }
    }

     

    • def "self invocation"(): 트랜잭션이 적용되지 않으면 테스트 코드는 pass (result.size() == 1 이므로)
    테스트 코드 실행 결과 (self invocation)

    • 테스트코드 실행 결과, 롤백되지 않는 것을 확인함
    • bar()에서 내부의 Transactional이 선언된 foo()메소드를 실행 (self invocation)
      proxy 기반의 AOP를 사용했을때는, self invocation이 일어나면 부가 기능들이 실행되지 않음!!

    문제 해결법
    1. @Transactional 위치를 외부에서 호출하는 bar() 메소드로 이동
    2. 객체의 책임을 최대한 분리하여 외부 호출 하도록 리팩토링 (내부 호출하지 않도록)

    트랜잭션을 읽기 전용으로 설정 가능 (@Transactional (readOnly=true)

    • 읽기 전용으로 설정하면 JPA에서 snapshot 저장 및 Dirty Checking 작업을 수행하지 않아서 성능적으로 이점이 있음
    Transactional의 적용 우선 순위
    • class 보다 method의 우선순위가 높음
    • class에 @Transactional (readOnly = true)으로 설정하고, update가 발생하는 method에만 readOnly=false 우선 적용 (SimpleJpaRepository 참고)

    • SimpleJpaRepository - class에 @Transactional(readOnly=true)이 선언되어 있음

    • 데이터 변경이 일어나는 메소드 (ex. deleteById(ID id))는 @Transactional 어노테이션을 추가로 적용함
    • 클래스는 readOnly=true이지만 메소드 우선순위가 더 높기 때문에 트랜잭션이 실행됨

    • 데이터 변경이 일어나지 않는 메소드 (ex. findAll())는 따로 @Transactional 어노테이션을 적용하지 않음
Designed by Tistory.