JPA에서 Join과 List Entity와 DTO

윤태호·2023년 5월 21일
0

Join

MySQL에서 Join을 하는 것은 어렵지 않았습니다. 외래키가 설정 되어있다면, join과 on을 사용해서 쿼리를 통해 조회가 가능했습니다.
JPA에서는 과연 Join을 어떻게 하는지 봅시다!!!


JPA 공부를 오래해서 그런지...마지막 프로젝트를 대부분 Query Dsl로 짜서 그런지 많이 까먹었다...
그래서 필자는 이 전 글처럼 @ManyToOne과 @OneToMany 설정이 완료 되어있으면 자동적으로 Join 쿼리가 나가는 줄 알았습니다.

    @Transactional
    public PostResponse getPost() {
        List<Post> post = postRepository.findAll(); 
        return null;

위처럼 Post에 대해 List로 모든 Post를 조회했을 때 과연 Join 쿼리가 나가는지 봅시다.

Post 조회에 대한 쿼리 뿐만 아니라 Comment에 대한 쿼리까지 날라가는 것을 확인 할 수 있었습니다.
지금은 댓글을 하나와 게시글 하나여서 딱 2개만 나가지만, 댓글이 계속 늘어난다면 매우 많은 쿼리가 생길 것 입니다.(N+1 문제가 발생)

드디어 N+1 문제를 다루게 되었습니다. 그러면 왜 N+1 문제가 발생하는지 이해하고 넘어갑시다!!!

바로 저번 글에서 다뤘던 지연 로딩 때문입니다!!!
지연 로딩에 대한 설명은 저번 글을 참조하시길 바라면 N+1이 일어나는 과정을 간단히 정리하고 넘어가겠습니다.
1. 식별자(ID)를 통해 주 엔티티인 Post에 엔티티를 조회를 시작합니다.
2. Post 내부에 있는 정보들에게 접근을 시작합니다. getComment()
3. getComment()가 실행되며 Comment 엔티티에 접근하는 쿼리문을 날립니다.(N+1)문제 발생!!!

해결방안

N+1에 대한 문제와 해결 방안은 이미 많은 블로그에서 많이 다루고 있으니 간단히 말하고 넘어가겠습니다!!

  • @Query Fetch Join 쿼리
  • Query Dsl
  • @EntityGraph
  • default_batch_fetch_size

    필자는 저번 프로젝트 때는 Query Dsl을 통해 해결했고, 이번 프로젝트에서는 Fetch Join을 통해 해결했습니다.

    @Query("SELECT p from Post p join Fetch p.commentList order by p.created_at desc limit 50")
    public List<Post> getPost();

    위처럼 Code를 작성했습니다.

    위처럼 N+1 쿼리가 발생하지 않고 join 쿼리를 통해 가져오는 것을 확일 할 수 있었습니다.

필자도 아직 @EntityGraph와 default_batch_fetch_size를 활용해보지 못해 글을 남기지 못하겠습니다.

  • default_batch_fetch_size는 설정을 해주면 상위 ID에서 size 만큼 in쿼리를 통해서 찾아온다고 합니다!!!!

추후에 사용하면 또 글을 남겨보겠습니다 ㅎㅎ

StackOverFlow와 Jackson??

필자는 Fetch Join을 끝내고 이제 PostMan을 통해 제대로 결과를 내뱉는지 보고 싶었습니다.
그래서 GET 요청을 날렸지만 에러만 돌아왔습니다 ㅜㅜ
에러 화면 아래와 같습니다!!

맨 처음에는 StackOverFlow가 보이길래... 순환참조가 일어나나(실제로 일어나는 중.....)???
그래서 저의 코드를 다시 뜯어보니...멍청하게 놓친 것을 알았습니다.

@Builder
@Data
public class PostResponse {
    private List<Post> data;
}

Post엔티티 그 자체를 내보내고 있었습니다 ㅠㅠ

엔티티를 내보내는데 왜 순환 참조가 일어나는지 의문이 드실 겁니다!!!
바로 Post와 Comment가 서로 양방향 관계를 가지고 있기 때문인데요.

  • Post 객체를 JSON으로 직렬화하려고 할 때, Post 객체 안의 Comment 객체를 직렬화하고, Comment 객체 안의 Post 객체를 직렬화하고, 이런 식으로 무한히 반복하게 되어 순환 참조가 발생합니다!!

그래서 우리는 DTO를 만들어서 데이터를 전달해서 넘겨주는 것입니다!!!
코드를 꼼꼼히 짭시다..ㅎㅎ 테스트만 해본다고 대충 짜다가 시간만 날렸네요 ㅎㅎ

이 과정이 근데 사실 좀 많이 귀찮습니다. For문을 쓰거나 Stream을 쓰면서 Entity를 DTO로 다 변환해야하기 때문입니다. ㅠㅠ 저의 코드를 한번 보실까요??

@Transactional
  public PostResponse getPost() {
      List<Post> posts = postRepository.getPost();
      List<ListPostResponse> listPostResponse = posts.stream().map(i->{
          ListPostResponse listPostResponse1= ListPostResponse.builder()
                  .title(i.getTitle())
                  .commentList(i.getCommentList().stream().map(j->{
                      CommentListDTO commentListDTO1 = CommentListDTO.builder()
                              .content(j.getContent())
                              .writer(j.getWriter())
                              .build();
                      return commentListDTO1;
                  }).collect(Collectors.toList()))
                  .content(i.getContent())
                  .views(i.getViews())
                  .image(i.getImage())
                  .likes(i.getLikes())
                  .build();
          return listPostResponse1;
      }).collect(Collectors.toList());
      PostResponse postResponse = PostResponse.builder().data(listPostResponse).build();
      return postResponse;
  }

엄청 길어졌죠???
Post도 List로 받고 댓글도 List로 받기에 Stream을 두번 써주고 그 사이에서 Builder로 데이터를 주입해주고...많이 귀찮지만 어쩔 수 없죠^^ 적응해야합니다!!!!

글을 마치며

JPA와 Spring Boot를 복습 중인데 고도화된 기술에 관심을 가지는게 즐겁지만, 근본인 JPA에 대해 많이 까먹었다는 걸 다시 한번 깨우치며... 그래도 많이 공부하고 복습하는 시간을 가질 수 있어서 좋았습니다!!!!
여러분들도 아직 취준생이거나 주니어면 코테만 너무 풀지말고...JPA를 다시 제대로 코드를 직접치면서 공부해보시는 것도 좋은 방법인거 같아요ㅎㅎ

profile
성장하는것을 제일 즐깁니다.

0개의 댓글