-
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이 일어나면 부가 기능들이 실행되지 않음!!
문제 해결법
- @Transactional 위치를 외부에서 호출하는 bar() 메소드로 이동
- 객체의 책임을 최대한 분리하여 외부 호출 하도록 리팩토링 (내부 호출하지 않도록)
트랜잭션을 읽기 전용으로 설정 가능 (@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 어노테이션을 적용하지 않음