오늘은 Spring JPA 프로젝트를 진행하며 가장 많은 문제를 야기 시킨 한 녀석 그리고 반드시 알고써야할 개념들을 정리 해보려한다.
스프링부트 JPA 를 이용하여 프로젝트를 진행할 경우 데이터를 읽고 쓰고 저장하는 일련의 모든 데이터 연산에 트랜젝션 단위가 이용된다.
트랜젝션 단위를 사용하는 이유는 아래와 같이 데이터 정합성을 위해 이용하지만 오늘 중점적으로 다뤄볼 한가지 이유를 더 추가하고자 한다.
- 트랜젝션이 시작되었을때 다른시점에 시작된 트랜젝션과 서로 영향을 줄수 없어 격리성이 유지되며 에러 발생시 Rollback 하는 방법으로 일부만 남아있는것이 아닌 정상 전체저장 비정상 전체롤백을 통해 그 원자성을 유지함.
- 영속성 컨텍스트의 엔티티 자원관리 기능을 효율적으로 사용하기 위함.
위의 이점들을 보아 Transactional 이 필연적으로 사용해야하는것으로 보여지며 실제로도 사용해야한다.
잘못사용했을경우 잘못사용했을경우 그로인해 생기는 부수적인 효과가 어마어마 하기때문에 지금까지 현업에서 맞으면서 배운 케이스를 공유하고자 한다.
1. saveAndFlush() 는 flush() 를 하지 commit 하지 않는다.
위 코드에서 m.getUsername(); 을 찍었을때 과연 Patrick이 찍혔을까 ? 정답은 바로 NullpointerException 이다.
그 이유는 정말 간단하다 REQUIRES_NEW 키워드는 검색하면 쉽게 찾을수 있다시피 부모트랜젝션과 떨어져
독립적인 트랜젝션을 생성하는것이다.
디비격리 수준 1단계 이상
Mysql Oracle PostgreSql 등 하나의 트랜젝션이 테이터를 구워삶던 삭제하던 그 트렌젝션이 최종 산출데이터를 커밋하지 않는 이상 다른 트랜젝션은 변화된 데이터를 읽을 수 없다.
하여 test() 메소드가 save() 메소드를 호출하여 데이터를 flush()하여 데이터를 디비에 밀어 넣었지만 test() 메소드가 종료되지 않은 상태 즉 커밋이 이뤄지지 않아 getMember() 메소드에서는 새로이 추가된 14번 멤버의 데이터를 읽어올 수 없다.
2. 동일한 @Bean 내부에서의 @Transactional 호출.
@Transactional 어노테이션이 적용되어있는 메소드와 그 해당하는 클래스가 외부 SpringBean에서 호출되는 즉시 트랜젝션을 품고있는 클래스와 그 메소드는 Proxy 로 등록되어 메소드 시작전 트랜젝션을 오픈하는 로직과 메소드가 종료 되었을 경우 커밋하는 로직을 추상화하여 비즈니스 로직에 적용하는 Spring AOP 디자인패턴이 적용되어있다.
두가지 대표적인 Proxy 구현체가 존재하는데 하나의 방식은 Jdk(Dynamic Proxy) 하나는 CGLib 두가지 방식이 있는데 둘의 차이는 Dynamic Proxy 는 프록시 등록 타겟이 Interface 를 상속하고 있으며 Reflection 객체를 이용하여 타겟의 메타데이터를 가져와 CGLib 보다 무겁게 돌아간다는 단점이 있다.
CGLib 는 target 객체의 인터페이스의 상속이 없어도 프록시로 등록이 가능하다는 장점이 있다.
프록시에 대하여 자세한 내용을 다루는 시간이 아니니 프록시에 대한 깊이있는 내용은 다음에 남겨보려 한다.
본론으로 돌아가서 거두절미하고 스프링 공식문서는 @Transactional 메소드 내부 호출시 프록시가 적용되지 않는다고 적혀있다 즉 Self-invocation 을 하지 않는것이다.
프록시 개념이 어렵다면 싹~ 다 몰라도 좋다 그냥 내부 호출시 @Transactional 이 먹지 않는다 그냥 없는것과 같다 정도로 기억하자.
@Service
@Slf4j
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final MemberServiceHandler handler;
public String test(){
Member m = Member.builder()
.id(14L).username("Patrick").age(24)
.build();
getMember(m);
return "Is it OK??";
}
@Transactional
public void getMember(Member m){
memberRepository.save(m);
throw new RuntimeException();
}
}
위 test() 코드 호출시 checkedException 이 발생하였다 과연 롤백이 이뤄질까 아니면 롤백이 이뤄지지 않고 저장될까.
정답은 위에서 언급했다시피 절대 롤백되지 않는다. 혹시 궁금하다면 한번 실행해봐도 좋을것같다.
다시한번 확인하자면 현재 getMember() 메소드에는 @Transactional 이 달려있지만 Self_invocation 을 지원하지 않기때문에 트랜젝셔널 어노테이션이 안달려있는것과 같은상태이다.
Required
부모 트랜잭션이 존재하면 부모 트랜잭션에 합류
- 자식/부모 rollback 이 발생된다면 자식과 부모 모두 rollback 이 발생한다.
Requires New
새로운 트랜잭션을 만듦.
- Nested 한방식이 이뤄지더라도 rollback 은 각각 이뤄진다.
하지만 자식 Transaction 에서 예외 처리 하지 않은경우 부모 Transaction 으로 전파가 이뤄지기 때문에 전체(부모/자식) rollback 이 일어날 수 있음.
Mandatory
부모 트랜잭션이 존재하지 않는경우 예외 발생
Nested
부모 트랜잭션이 존재하지 않는다면 새로운 Transaction 발생, 부모 트랜잭션이 있는경우 Required 와 동일.
Supports
진행중인 트랜잭션이 있으면 합류 없으면 없는 상태로 진행.
Never
부모 트랜잭션이 있는경우 예외 발생
정리
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#spring-data-tier
1.4.6. Using @Transactional
https://stackoverflow.com/questions/29650355/why-in-spring-aop-the-object-are-wrapped-into-a-jdk-proxy-that-implements-interf