REQUIRES_NEW 를 사용하는 상황을 피하자[Spring JPA 코드 생활백서]
서론
현재 SpringJpa 프로젝트를 하며 겪고있는 문제점들을 줄여나가 보고자 개인 JPA 컨벤션을 만들어보고자 한다.
REQUIRES_NEW 를 사용할 경우 생길 수 있는 이슈 1번
- 같은 식별자를 갖고있는 객체를 서로 다른 영속성 컨텍스트에서 수정이 이뤄진 경우.
아래 코드를 살펴보자
@Transactional
public String test(){
Member testMember1 = repository.findByMemberByMemberId(2L);
handler.innerTest();
testMember1.changeAge(35);
return null;
}
test() 메서드는 컨트롤러에서 호출 된후 innerTest 라는 메소드를 호출한다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerTest(){
Member testMember2 = memberRepository.findByMemberByMemberId(2L);
testMember2.changeName("Patrick");
}
현재 코드 실행전 데이터베이스에 들어가있는 데이터는 아래와 같다
2번 멤버의 이름은 테스트이며 나이는 33이다
데이터베이스는 마리아디비를 사용했으며 격리수준 2단계 REPEATABLE_READ 가 적용돼있다.
결과를 예측해보자
- REQUIRES_NEW 를 사용했으니 이름은 Patrick으로 바뀌고 나이는 35 으로 바뀔것이다.
- 이유는 모르겠고 에러가난다
- 바뀌라는 이름은 안바뀌고 나이만 35로 바뀐다.
결과는 3번
사실 메소드 2개만 꼴랑 있을경우 위같은 코드에서 생기는 문제는 금방 찾아낼 수 있다 하지만 원리를 알아도
위와같이 코드가 작성되어있고 트랜젝션이 4중 5중으로 들어가면 어디서 바뀌고 안바뀌는지 일일이 찾아 헤매야할것이다.
위코드 실행결과는 REQUIRES_NEW에서 적용한 코드는 단 한~~개도 반영되지 않는다이다.
이유를 살펴보자
쉽게 설명하기 위하여 test() 에서 오픈한 트랜젝션을 T1 innerTest() 에서 오픈한 트랜젝션을 T2라 하자
T1 에서 열어놓은 트랜젝션을 살펴보면
- testMember1 객체를 가져올때 위 영속성 컨텍스트 1차캐시에는 Id는 2번을 가진 엔티티가 캐시에 올라간다.
- innerTest 메소드에 testMember2를 가져오면 새로운 영속성 컨텍스트가 열리며 아래와 같이 서로다른 영속성 컨테이너에 올라간다.
- T2 컨테이너에서 맴버의 객체를 변화한다
- 이후 T2트랜젝션이 종료될때 T2 트랜젝션에 해당하는 영속성 컨테이너가 종료되면서
해당 변경된 객체를 flush 후 디비에 SQL 문을 전송한후 커밋한다.
- 다시 T1 으로 돌아왔을때 이제 T1에 있는 객체의 나이가 35으로 변경된다.
- 이후 id -> 2 이며 name 은 테스트 나이는 35인 객체가 들어가는것이다
REQUIRES_NEW 트랜젝션 컨테이너의 변경값은 디비에 한번 커밋까지 됐지만 최종 T1의 컨테이너 트랜젝션에 오버라이드 된것이다.
사실 이와같은 케이스가 메서드가 2개일경우 참 바보같은 실수를 하였구나 이런걸 내가 왜하냐 라고 생각했을때 코드가 고도화 되고 트랜젝션이 엉키고 엉키다 보면 이런일은 언제든지 발생할 수 있다.
REQUIRES_NEW 를 사용할 경우 생길 수 있는 이슈 2번
1번 이슈에서 볼수 있듯 아니 그러면 REQUIRES_NEW 에서 사용한 맴버 객체를 리턴하면 되지 않습니까!
결론 -> 이방법도 좋지는 않다 쓰지 말자
현재 디비 상태는 이렇다.
@Transactional
public String test(){
Member testMember1 = handler.innerTest();
testMember1.changeAge(35);
return null;
}
위와 같이 test() 메소드는 innerTest() 메소드를 호출한다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Member innerTest(){
Member testMember2 = memberRepository.findByMemberByMemberId(2L);
testMember2.changeName("Patrick");
return testMember2;
}
위 코드가 종료될시 결과값을 예측해보자
1. REQUITES_NEW 가 실행되고 test에서 change를 했으니 이름은 Patrick 이고 나이는 35로 바뀔것이다.
2. 뭔지 모르겠지만 에러가 난다.
3. 나이만 35로 바뀔것이다.
4. 이름만 바뀐다.
정답은 4번 이다.
그 이유는 test() T1 트랜젝션이 시작할때 T1 컨테이너에는 2번 맴버를 영속성컨텍스트에 저장한적이 없다.
특 T1 컨테이너는 2번 맴버가 영속성 컨텍스트에 없다. T2 에서 2번 맴버를 저장하고 디비에 플러시 하고 T1 트랜젝션 컨테이너에 맴버를 반환한다고 해서 T1 영속성컨텍스트에 올라가는것이 절!대 아니다.
- 이 코드에서 문제는 저렇게 반환했을대 비영속상태인 testMember1을 코드가 길어지면 길어질수록
(물론 이만한 실수는 그 전에 장애를 발생시키겠지만) testMember1-> 비영속 객체를 다른 개발자들이 신나게 갖고 놀것이다. 하지만 디비에는 아~~무런 일도 일어나지 않게된다.
개선방법
- 최상위단에 트랜젝션이 걸려있고 그 아래로 2중 3중으로 타고 들어가는 코드를 피하자.
어디서 최초로 불러와졌고 어디서 수정해서 변경감지가 되어 데이터 베이스에 들어가는지 찾기 매우 어려워진다. - 엔티티에 대한 수정이 많을경우 DTO 객체를 이용하자
- 엔티티의 변경은 곧 디비의 변경과 같다 라는 경각심을 갖고 코드를 치도록하자.
결론
하이버네이트 사용시 같은 아이디가 다른 영속성컨테이너에서 다른주소값으로 수정되는 상황은 무조건 피하도록하자.
Author And Source
이 문제에 관하여(REQUIRES_NEW 를 사용하는 상황을 피하자[Spring JPA 코드 생활백서]), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dkajffkem/REQUIRESNEW-를-사용하는-상황을-피하자Spring-JPA-코드-생활백서저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)