TeamGuu / teamguu-backend

University Teamguu project API server
0 stars 0 forks source link

About transaction #44

Open Minuooooo opened 1 year ago

Minuooooo commented 1 year ago

서비스 로직에 무조건적인 @Transactional 적용은 피해야 한다. 상황에 맞게 적용하려면 어떻게 해야할까?

weejinyoung commented 1 year ago

선언적 트랜잭션인 @Transactional은 객체 외부에서 메소드를 호출할 때만 적용합니다 내부에서만 쓰이는 메소드엔 @Transactional이 적용이 안된데요 그래서 내부에서 쓰이는 메소드에 꼭 트랜잭션을 적용하고 싶다면 TransactionTemplate을 써서 할 수 있다고 합니다

그래서 해결방법이라고 하긴 뭐하지만, 외부에서 호출되는 API 에다가만 @Transactional을 붙이는 방법이 있겠죵 그 메소드의 모든 과정이 트랜잭션 관리가 됩니다

    @Transactional(readOnly = true)
    override fun getCurrentMember() =
        try {
            getMemberByEmail(SecurityContextHolder.getContext().authentication.name)
        }  catch (e: MemberNotFoundException) {
            throw CurrentMemberNotFoundException()
        }

    private fun getMemberByEmail(email: String) =
        memberRepository.findByEmail(email)
            ?:throw MemberNotFoundException("이메일")

하지만 이렇듯 선언적 트랜잭션에는 몇가지 단점이 보입니다

  1. 메서드 레벨에 AOP 가 적용되기 때문에 트랜잭션 단위도 메서드 레벨로 적용(메서드 내에서 지정 불가능)\
  2. self invocation 에서 트랜잭션 적용 불가

선언적 트랜잭션만으로 트랜잭션 관리가 힘들때는 TransactionTemplate의 excute 메소드로 트랜잭션을 관리하는 방법을 써보는걸 고민해봅시다

Minuooooo commented 1 year ago

TransactionTemplate을 활용해서 하는 방법도 알아봐야겠네요. 이해하기 쉽게 정리해주셔서 감사합니다!

이제 제가 드릴 의견은,

public PostInfoResponseDto getPostInfo(Long postId) {
        return PostInfoResponseDto.toDto(findPost(postId));
    }

    @Transactional
    public void editPostInfo(EditPostInfoRequestDto editPostInfoRequestDto, Long postId) {
        findPost(postId).editPost(editPostInfoRequestDto.getTitle(), editPostInfoRequestDto.getPrice(),
                editPostInfoRequestDto.getContent(), editPostInfoRequestDto.getPlace());
    }

    public void deletePost(Long postId) {
        postRepository.delete(findPost(postId));
    }

    private Post findPost(Long postId) {
        return postRepository.findById(postId).orElseThrow(PostNotFoundException::new);
    }

위의 코드를 보시면, 일단 게시물의 id를 매개변수로 스프링 데이터 JPA를 활용하여 Post를 불러옵니다. 이를 활용하여 게시물의 상세 정보를 보여주는 getPostInfo와 게시물을 삭제하는 deletePost에는 @Transactional이 적용이 안되어 있습니다. 이유는 스프링 데이터 JPA에서 CRUD를 제공하는 기본적인 메소드는 자동으로 트랜잭션을 적용시켜준다고 합니다. 그래서 자동으로 적용시켜주는 것을 이용하여 Post 객체를 불러와 해당 엔티티의 속성이 변화한다거나 그러지 않는 한은 @Transactional을 적용하지 않아도 된다는 말이 됩니다. 그래서 엔티티의 속성 변화를 일으키는 editPostInfo 메소드에만 @Transactional을 적용시켜주었습니다. 하지만 이 부분에서 확신이 서지 않는 메소드가 많을 것이므로 앞으로의 학습이 요구됩니다.

weejinyoung commented 1 year ago

오우 아주 맛있는 사실이네요 JpaRespository는 정말 무적인 것 같은 느낌이 듭니다 Chat GPT와 JpaRepository가 없었으면 저도 시클보 열심히 들으면서 장래를 계속 고민했겠죠? Transaction에 대해 알아낸게 있다면 계속 이 이슈에 올려보겠습니다

Minuooooo commented 1 year ago

너무 맛있죠 시클보와 블체가 있어서 더욱 열심히 개발할 수 있는 것 같습니다 역시 수업 시간에는 코딩이죠 Chat GPT와 불화가 있으셨는데 잘 해결하셨으면 좋겠고, Spring Data Repository는 무적입니다! 저도 학습한 것이 있다면 이슈에 깔끔하게 정리해서 올리도록 하겠습니다

Minuooooo commented 1 year ago

일단 @Transactional을 통해 안정적으로 select 쿼리를 생성하지만 @EntityGraph를 통해 fetch join을 활용하면 select 쿼리 한 번과 left join 쿼리 1개가 나오는데 성능 비교를 좀 해봐야 할 것 같습니다 @Transactional, @EntityGraph 둘 다 활용하는 게 좋지만 @EntityGraph를 활용할 수 없을 때는 @Transactional을 활용하는 게 좋을 것 같습니다 확실히 상황 별로 선택하여 사용하는 게 좋은 것 같습니다 이 부분에 대해서는 지인에게 물어보거나 좋은 자료를 찾아내야 할 것 같습니다.

weejinyoung commented 1 year ago

우리가 몇일 밤을 거쳐 깨달은 영속성 컨텍스트, 지연로딩에 대한 사실

  1. 비영속 엔티티 객체는 지연로딩이 불가하다, Transaction이 끝나면 세션도 끝나기 때문에 준, 비 영속 상태가 된다
  2. @Transactional 은 비영속을 영속으로 만들어주진 못한다 그저 비영속으로 돌아가지 못하게 잡아줄 뿐, 비영속 엔티티 백날 매개변수로 넘겨줘봤자 소용없다
  3. @Transactional 이 걸린 메소드의 모든 하위 메소드들도 트랜잭션 관리가 된다, 컨트롤러가 직접 호출하는 메소드에 트랜잭션을 걸어주면 그 쓰레드는 트랜잭션 관리가 된다
  4. @EntityGraph로 즉시로딩을 하면 쿼리를 날려 실제 엔티티 객체를 가져오기 때문에 프록시 객체에 접근해서 생기는 예외인 Lazy exception이 생기지 않는다
  5. 따라서 @Transactional과 @EntityGraph는 상황별로 쓰이게 된다, 상황을 판단하는 힘을 기르자

아아 이제야 깨닫습니다 영버지....

Minuooooo commented 1 year ago

5번 항목에 대한 예시를 자세하게 들자면, Many To One 관계인 Match, Team / Team, Member 상황에서 Match 엔티티를 가져올 때 연관된 Team, Member 엔티티를 가져와야 할 때 MatchRepository에서 @EntityGraph를 사용하여 Team 엔티티를 가져와도 Match.getTeam().getCaptain()을 호출하게 되면 LazyInitializationException이 발생하게 됩니다. 왜냐하면 Team 엔티티를 가져온 것이지 Member 엔티티까지는 가져오지 못 했기 때문이지요 그래서 이런 상황에는 @Transactional을 활용한다든지 아니면 다른 방법이 있는지 학습할 필요가 있습니다