[JPA,SpringBoot] KnockKnock 개발일지 - 0103

✔오늘의 목표

  1. ✔post, comment와 member Entity 연결하기
  2. ✔post, comment와 member Entity 연결 완료 테스트 하기
  3. ✔신고 기능 CRUD 구현
  4. ✔신고 기능 테스트 하기
  5. ✔메세지 기능 CRUD 구현
  6. ✔메세지 기능 테스트 하기

오늘의 이슈

  1. 또 다시 해결이 안되고 있는 Test 에서의 @Transactional delete 쿼리 안나가는 문제..
  2. QClass 수정사항 반영해서 재생성하기
  3. 💥메세지 기능 관련 연관관계 설계 고민

1. Test에서 delete 쿼리가 안나가는 현상

어쩌다 테스트 코드 이것저것 만져보다 보니 이상한 점이 생겼다.
해결했다고 생각했던 test에서 delete 쿼리가 안나가는 현상..이 내가 생각했던 원인이 아닌것 같았다.
왜냐면 똑같은 코드의 post 삭제 테스트 코드는 잘 돌아가고 comment 삭제 테스트 코드만 안돌아가기 때문이였다 ㅠㅠㅠㅠㅠㅠ

 @Test
    public void 게시글삭제(){
        //given
        PostSaveRequest postSaveRequest = new PostSaveRequest();
        postSaveRequest.setTitle("새 글입니다");
        postSaveRequest.setContent("반가워요");
        Member member = memberRepository.findByNickName("테스트멤버1");
        postSaveRequest.setWriterId(member.getId());
        String[] tags = {"인사","게시글삭제"};
        postSaveRequest.setHashTags(tags);
       Post savedPost =  postService.save(postSaveRequest);
        CommentRequest commentRequest = new CommentRequest();
        commentRequest.setComment("this comment should be deleted");
        commentRequest.setPostId(savedPost.getId());
        commentRequest.setWriterId(member.getId());
        Comment targetComment = commentService.save(commentRequest);
        //when
        postService.delete(savedPost.getId());
        //then
        Post targetPost = postRepository.findOneById(savedPost.getId());
        HashTag targetHashTag = hashTagRepository.findByTag("게시글삭제");
        HashTag targetHashTag2 = hashTagRepository.findByTag("인사");
        List<PostHashTag> targetlist = hashTagRepository.findPostHashTags(targetHashTag2.getId());
        Assertions.assertThat(targetPost).isNull();
        Assertions.assertThat(targetHashTag).isNull();     Assertions.assertThat(targetHashTag2).isNotNull();
        Assertions.assertThat(targetlist.size()).isEqualTo(4);
        Comment comment = commentRepository.findOne(targetComment.getId());
        Assertions.assertThat(comment).isNull();
    }

여기서는 post.remove 메소드에 em.flush()를 따로 적어두지 않아도 테스트가 통과한다.
그런데

 public void 댓글삭제(){
        //given
        List<Post> posts = postRepository.findByTag("영어");
        Member member = memberRepository.findByNickName("테스트멤버1");
        Post post = posts.get(0);
        CommentRequest commentRequest = new CommentRequest();
        commentRequest.setComment("this comment should be deleted");
        commentRequest.setPostId(post.getId());
        commentRequest.setWriterId(member.getId());
        Comment savedComment= commentService.save(commentRequest);
        Long id = savedComment.getId();
        //when
        commentService.delete(savedComment.getId());
        //then
        List<Post> posts2 = postRepository.findByTag("영어");
        Post post2 = posts2.get(0);
        List<Comment> comments = post2.getPostcomments();
        Assertions.assertThat(comments.size()).isEqualTo(0);
    }

이 코드에서는 comment.remove 메소드에 em.flush()를 해주지 않으면 아예 delete 쿼리가 나가지 않는 문제가 발생한다.

정말 도저히 모르겠어서 인프런에 글 썼다... 빨리 해결되길 엉엉


1-1. 해결

인프런에서 온 답장을 토대로 문제를 이해할 수 있었다.
이 문제의 핵심은 다음과 같다.

  1. Rollback 처리된 코드의 query로그는 찍히지 않는다.
  2. Transactional 처리된 Test 코드 안의 Transactional 선언된 외부클래스 메소드는 전파되어 Test 메소드의 트랜잭션에 합류하여 트랜잭션이 무효화된다.
  3. JPQL 사용시 자동으로 flush 되며 만약 JPQL 코드가 1차캐시에서 조회해 쿼리문이 나가지 않는 경우 flush도 되지 않는다.

따지고 보면 지난 개발일지에서 내가 정리한 쿼리가 나가지 않는 이유는 1,2 번과 동일하고 따라서 맞는 말이다.
이번 이슈는 3번을 알지 못해서 온 혼란이였다.
다시 한 번 코드를 보면서 자세히 이해해보자🤸‍♀️

이 문제의 핵심은 PostRepository의 findByTag 메소드에 있었다.

 public List<Post> findByTag(String tag){

        List<Post> posts = queryFactory.selectFrom(post).distinct()
                .innerJoin(postHashTag).on(post.id.eq(postHashTag.post.id))
                .where(postHashTag.tag.contains(tag))
                .orderBy(post.timestamp.desc())
                .fetch();
        return posts;
    }

내가 findByTag 메소드를 QueryDSL로 구현했기 때문에 당연히 이 메소드도 JPQL이 실행되고 따라서 메소드는 실행되기 직전에 flush 한다.

 @Test
    public void 게시글삭제(){
        //given
        PostSaveRequest postSaveRequest = new PostSaveRequest();
        postSaveRequest.setTitle("새 글입니다");
        postSaveRequest.setContent("반가워요");
        Member member = memberRepository.findByNickName("테스트멤버1");
        postSaveRequest.setWriterId(member.getId());
        String[] tags = {"인사","게시글삭제"};
        postSaveRequest.setHashTags(tags);
       Post savedPost =  postService.save(postSaveRequest);
        CommentRequest commentRequest = new CommentRequest();
        commentRequest.setComment("this comment should be deleted");
        commentRequest.setPostId(savedPost.getId());
        commentRequest.setWriterId(member.getId());
        Comment targetComment = commentService.save(commentRequest);
        //when
        postService.delete(savedPost.getId());
        //then
        Post targetPost = postRepository.findOneById(savedPost.getId());
        📌 여기서 flush가 이루어진다!
        HashTag targetHashTag = hashTagRepository.findByTag("게시글삭제");
        HashTag targetHashTag2 = hashTagRepository.findByTag("인사");
        List<PostHashTag> targetlist = hashTagRepository.findPostHashTags(targetHashTag2.getId());
        Assertions.assertThat(targetPost).isNull();
        Assertions.assertThat(targetHashTag).isNull();     Assertions.assertThat(targetHashTag2).isNotNull();
        Assertions.assertThat(targetlist.size()).isEqualTo(4);
        Comment comment = commentRepository.findOne(targetComment.getId());
        Assertions.assertThat(comment).isNull();
    }

게시글을 삭제하는 테스트가 통과할 수 있었던 이유는 findByTag 메소드 전에 flush가 일어나기 때문이다. 따라서 delete 쿼리문이 모두 flush 되어 DB에 동기화되기 때문에 원하는 대로 테스트가 잘 통과할 수 있었던 것이다.

 public void 댓글삭제(){
        //given
        List<Post> posts = postRepository.findByTag("영어");
        Member member = memberRepository.findByNickName("테스트멤버1");
        Post post = posts.get(0);
        CommentRequest commentRequest = new CommentRequest();
        commentRequest.setComment("this comment should be deleted");
        commentRequest.setPostId(post.getId());
        commentRequest.setWriterId(member.getId());
        Comment savedComment= commentService.save(commentRequest);
        Long id = savedComment.getId();
        //when
        commentService.delete(savedComment.getId());
        //then
        List<Post> posts2 = postRepository.findByTag("영어");
        Post post2 = posts2.get(0);
        List<Comment> comments = post2.getPostcomments();
        Assertions.assertThat(comments.size()).isEqualTo(0);
    }

한편 댓글 삭제 코드에도 delete 이후에 findByTag 메소드가 있다.
그런데 왜 이번에는 flush가 일어나지 않은 것일까?
그 이유는 바로 findByTag로 찾고자 했던 태그가 "영어" 였던 post 들이 전부
1차 캐시 안에 있어서 쿼리가 나갈 필요가 없었기 때문이다.
1차 캐시 안에서 바로 가져오니 당연히 update 될것도 없고 당연히 flush도 일어나지 않게 되는 것이다.

따라서 위 테스트 코드는 flush가 따로 이루어지지 않기 때문에 CommentRepository의 remove 메소드에 em.flush()를 따로 추가해야 한다.

2. QClass 수정사항 반영해서 생성하기

이 문제는 단순히 gradle reload(refresh)만 해서 되는 것이 아니였다
QClass 를 다시 빌드하기 위해서는

다음과 같이 Gradle -> Tasks -> other -> compileJava 를 실행해야 한다.
만약 이와 같은 Gradle 창이 뜨지 않은 경우 View -> Tool Windows -> Gradle 로 들어가면 뜬다!

3. 💥메세지 기능 관련 연관관계 설계 고민

어떻게 하면 효율적인 설계인지 유독 어려웠던 부분이다.. ㅠ
일단 유저메세지마다 Sender와 Receiver 가 다르다는 것이 헷갈리는 포인트였고 (보통은 Member 한 명 만으로 양방향 연관관계 매핑을 했었으니 ㅠ)
MessageDialog에서 Receiver가 누구냐에 따라 보이는 창이 달라지는데 그걸 어떻게 다룰 것인지가 의문이였다.


SendMessage 와 ReceiveMessage를 UserMessage의 상속을 받은 엔티티로 설정해서 각자 Member와 연결해주는건 어떨까?
-> 이렇게 하면 하나의 메세지를 send 와 receive 두개로 복제해야 하기 때문에 데이터 양이 배가 된다.
UserMessage에 sender 와 receiver 각각의 id를 주고 MessageDialog와 연관관계 매핑, MessageDialog를 유저마다 만들어서 유저와 매핑시키면 어떨까?
-> 일단 이 방법으로 구현해 보는 것으로 결정....


일단 구현도 했고 테스트도 통과하긴 했어서 1차적으로 기능상 내가 원하는 기능을 구현하기는 했다.

🎈내가 원했던 기능: 각 멤버별로 자신의 MessageDialog가 있고 해당 Dialog에 파트너별로 주고받은 Message가 있음, 서로 간의 첫 메세지를 주고받는 경우 새로운 dialog 엔티티가 생성되고 기존에 대화가 있을 경우 해당 dialog의 usermessage list에 이어서 메세지가 쌓이는 구조

그런데 쿼리문이 정말 어마어마하게 많이 나간다 ㅠㅠㅠ😭😣😭
내가 연관관계 매핑이 아니라 단순 id값만 갖게 하는 걸 처음 구현해봐서 query가 어떻게 나갈지 잘 모르는데.. 일단 코드에 해당하는 query문이 어떻게 나갈지 예상할 수 있도록 query공부를 좀 더 해야겠다는 생각이든다.

그것은... 나중에 유지보수하는 나에게 맡겨야겠지 깔깔
아무튼 오늘 개발은 여기서 끝!

내일 할 일

  1. @Transactional(readOnly = true)를 서비스 클래스에서 선언하는 이유 공부하기
  2. Controller와 API 공부하기 (JPAShop - JPA 실전 강의 복습)
  3. 간단한 API 하나 만들기

좋은 웹페이지 즐겨찾기