[Spring] M:N 서비스를 위해 필요한 데이터 처리

[Spring] M:N 서비스를 위해 필요한 데이터 처리

서비스를 위해 필요한 데이터의 처리를 해봅시다.


테스트 코드가 모두 정상적으로 실행되었다면 현재 데이터베이스에는 100개의 영화와 해당 영화의 이미지 파일들, 100명의 회원, 200개의 리뷰가 존재합니다. 이를 이용해 화면에서 필요한 데이터를 처리합니다.



1. MovieRepository - getListPage() 작성



1
먼저 목록 화면에서 사용할 데이터를 가져오고자 합니다. 목록 화면에서는 영화, 영화 이미지, 리뷰의 수, 평점 평균을 출력하고자 합니다. JPQL을 사용해 getListPage()를 작성합니다.
left outer join과 group by를 사용해 리뷰의 개수와 리뷰의 평균 평점을 구할 수 있습니다.

2. MovieRepositoryTests - testListPage() 작성



2

getListPage()를 테스트 하기 위한 testListPage()를 작성합니다.
mno를 기준으로 역순정렬 하며 10개씩 페이징하여 보여주도록 getLsitPage()의 매개변수를 위한 PageRequest 객체를 만든 뒤 실행합니다.

3. MovieRepositoryTests - testListPage() 결과



3

실행 결과 정상적으로 10개씩 영화를 가져오며 평균 평점 및 영화의 리뷰 개수가 출력되는 것을 확인할 수 있습니다.

4. MovieRepository - getListPage() 수정



4

getListPage()가 리뷰의 평점 평균 및 개수만 가져오는 것이 아닌 이미지까지 가져오도록 수정하고자 합니다. 이때 주의해야 할 점은 영화의 이미지를 가져올 때 N+1 문제가 생기지 않도록 작성해야합니다.
N+1 문제란 1번의 쿼리로 N개의 데이터를 가져왔는데 N개의 데이터를 처리하기 위해 필요한 추가적인 쿼리가 각각의 N개에 대해서 실행되는 상황입니다.
이미지를 가져올 때 max()를 사용해 첫 이미지를 가져오는 것이 아닌 단순하게 이미지를 출력하도록 쿼리를 작성하면 Movie와 MovieImage는 가장 먼저 입력된 이미지 번호와 연결되게 됩니다.

5. MovieRepositoryTests - testListPage() 결과



5

다시 한번 테스트 코드를 실행해 getListPage()를 테스트한 결과 N+1 문제가 발생하지 않고 영화별로 가장 낮은 번호의 이미지를 하나씩 가져오는 것을 확인할 수 있습니다.

6. MovieRepository - getMovieWithAll() 작성



6

이제 조회 페이지를 위한 getMovieWithAll()을 작성합니다.
getMovieWithAll()의 경우 getListPage()와는 달리 group by m을 사용하지 않기 때문에 이미지 모두를 불러옵니다.

7. 리뷰가 많은 영화 검색



7

테스트 코드를 수행하기 위해 리뷰 개수가 많은 영화를 검색합니다.
리뷰의 최대 개수인 5개의 리뷰가 존재하는 영화는 총 다섯개가 존재합니다.

8. 영화 이미지 개수 확인



8

앞서 리뷰가 5개인 영화들 중 이미지의 개수가 많은 영화를 테스트 코드에서 활용하려 합니다.
7번 영화의 경우 리뷰가 5개이며 이미지를 4개 가지고 있는 것을 확인할 수 있습니다.

9. MovieRepositoryTests - testGetMovieWithAll() 작성



9

getMovieWithAll()을 테스트하는 코드를 작성합니다.
위에서 확인한 7번 영화의 정보를 가져오도록 작성합니다.

10. MovieRepositoryTests - testGetMovieWithAll() 결과



10

실행 결과 getMovieWithAll()의 결과를 담아온 리스트의 경우 4개의 원소를 가지고 있고 각 원소들을 하나씩 출력한 결과 정상적으로 영화와 영화 이미지의 정보가 나오는 것을 확인할 수 있습니다.

11. MovieRepository - getMovieWithAll() 수정



11

이제 리뷰와 관련된 내용도 가져오도록 쿼리문을 수정합니다.
left join을 사용해 리뷰와 조인한 후 count(), avg()등의 함수를 이용하는데 이때 영화 이미지별로 group by를 실행해 그룹을 만들어 영화 이미지들의 개수만큼 데이터를 만들어 낼 수 있게 됩니다.

12. MovieRepositoryTests - testGetMovieWithAll() 결과



12

변경된 테스트 코드를 실행하면 위의 그림과 같이 해당 영화의 이미지 마다 리뷰 평균 평점과 개수가 나오는 것을 확인할 수 있습니다.

13. ReviewRepository - findByMovie() 작성



13

조회 화면에서 필요한 영화에 대한 리뷰를 가져오는 쿼리문을 작성합니다.
findByMovie()를 사용해 Review에서 필요한 데이터를 추출한 뒤 List에 담도록 작성합니다.

14. ReviewRepositoryTests - testGetMovieReviews() 작성



14

findByMovie()를 테스트하는 코드를 작성합니다.
7번 영화 객체를 생성한 뒤 7번 영화의 리뷰를 result에 담은 후 원소의 내용을 하나씩 출력하도록 합니다.

15. ReviewRepositoryTests - testGetMovieReviews() 결과



15

실행 결과 정상적으로 리뷰를 가져오는 것이 아닌 에러가 발생합니다. 이는 Review 엔티티의 Member에 대한 Fetch가 LAZY 방식이기 때문에 한번에 Review와 Member를 조회할 수 없기 때문입니다.
이 문제를 해결하기 위한 방법으로

  1. @Query를 이용해 조인처리
  2. @EntityGraph를 이용해 Review 객체를 가져올 때 Member 객체를 로딩
    이 있습니다.

16. ReviewRepository - findByMovie() 수정



16

두 번째 방법인 @EntityGraph를 통해 문제를 해결해봅시다.
@EntityGraph는 엔티티의 특정한 속성을 같이 로딩하도록 표시하는 어노테이션입니다. 특정 기능을 수행할 때 EAGER 로딩을 하도록 지정할 수 있습니다.
작성한 매개변수는 “member의 로딩 설정을 변경하며 member의 속성은 EAGER로 처리하고 나머지는 LAZY로 처리한다” 라는 의미입니다.

17. ReviewRepositoryTests - testGetMovieReviews() 결과



17

findByMovie()를 수정한 뒤 다시 테스트 코드를 실행한 결과 정상적으로 테스트가 진행되고 member가 left outer join 처리된 것을 확인할 수 있습니다.

18. ReviewRepository - deleteByMember() 작성



18

M:N 관계에서 현재와 같이 매핑 테이블을 구성하고 이를 엔티티로 처리한 경우 삭제를 수행할 때 주의해야 합니다.
Member를 삭제할 경우 해당 회원이 등록한 Review 역시 모두 삭제되어야 하기 때문입니다. 두 작업은 하나의 트랜잭션으로 관리해야 합니다.
이때 @Query를 이용해 where절을 지정한다면 review 테이블에서 하나씩 데이터를 삭제하는 것이 아닌 한번에 데이터를 전부 삭제하게 됩니다.

19. MemberRepositoryTests - testDeleteMember() 작성



19

회원을 삭제하는 테스트 코드를 수행할 때 유의할 점은 Member를 먼저 삭제하는 것이 아닌 Review를 먼저 삭제하는 것입니다.
deleteByMember()를 사용해 해당 회원이 작성한 리뷰를 먼저 삭제한 뒤 Member를 삭제해 FK 참조를 가지는 리뷰를 먼저 제거하는 것입니다.

20. MemberRepositoryTests - testDeleteMember() 결과



20

실행 결과 테이블의 데이터를 하나씩 접근하며 삭제하지 않고 한번에 리뷰를 삭제한 뒤 멤버의 정보를 삭제한 것을 확인할 수 있습니다.


© 2022. All rights reserved.