REQUIRES_NEW 를 사용하는 상황을 피하자[Spring JPA 코드 생활백서]

Patrick YOO·2022년 1월 26일
1
post-thumbnail

서론

현재 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 가 적용돼있다.

결과를 예측해보자

  1. REQUIRES_NEW 를 사용했으니 이름은 Patrick으로 바뀌고 나이는 35 으로 바뀔것이다.
  2. 이유는 모르겠고 에러가난다
  3. 바뀌라는 이름은 안바뀌고 나이만 35로 바뀐다.

결과는 3번

사실 메소드 2개만 꼴랑 있을경우 위같은 코드에서 생기는 문제는 금방 찾아낼 수 있다 하지만 원리를 알아도
위와같이 코드가 작성되어있고 트랜젝션이 4중 5중으로 들어가면 어디서 바뀌고 안바뀌는지 일일이 찾아 헤매야할것이다.

위코드 실행결과는 REQUIRES_NEW에서 적용한 코드는 단 한~~개도 반영되지 않는다이다.
이유를 살펴보자

쉽게 설명하기 위하여 test() 에서 오픈한 트랜젝션을 T1 innerTest() 에서 오픈한 트랜젝션을 T2라 하자

T1 에서 열어놓은 트랜젝션을 살펴보면

  1. testMember1 객체를 가져올때 위 영속성 컨텍스트 1차캐시에는 Id는 2번을 가진 엔티티가 캐시에 올라간다.

  1. innerTest 메소드에 testMember2를 가져오면 새로운 영속성 컨텍스트가 열리며 아래와 같이 서로다른 영속성 컨테이너에 올라간다.

  1. T2 컨테이너에서 맴버의 객체를 변화한다

  1. 이후 T2트랜젝션이 종료될때 T2 트랜젝션에 해당하는 영속성 컨테이너가 종료되면서
    해당 변경된 객체를 flush 후 디비에 SQL 문을 전송한후 커밋한다.

  1. 다시 T1 으로 돌아왔을때 이제 T1에 있는 객체의 나이가 35으로 변경된다.

  1. 이후 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 객체를 이용하자
  • 엔티티의 변경은 곧 디비의 변경과 같다 라는 경각심을 갖고 코드를 치도록하자.

결론

하이버네이트 사용시 같은 아이디가 다른 영속성컨테이너에서 다른주소값으로 수정되는 상황은 무조건 피하도록하자.

profile
자유인을 꿈꾸는 개발자

0개의 댓글