[ M:N(다대다)관계의 특징 ]
먼저, 테이블은 고정된 개수의 칼럼을 가지고 있기 때문에 M:N(다대다)의 관계를 실제 테이블로 설계할 수 없다.
예를 들어 여러 개의 상품이 있고, 여러 개의 카테고리가 있다고 생각한다.
여기서, 특정한 상품에 대해서 카테고리 정보를 추가하면 상품 하나가 '가전'인 동시에 '주방','계절 가전','신혼'과 같이 여러 개의 카테고리를 각각 칼럼에 담는다면 고정된 수의 칼럼으로는 처리할 수 없다는 문제가 생긴다.
상품번호 | 상품명 | 상품 카테고리 |
1 | 냉장고 | C1,C2,C3 |
2 | 세탁기 | C1,C2 |
3 | TV | C4 |
그림으로 표현하자면 위와 같다.
위의 그림과 같은 경우 ','를 사용해서 해당 상품이 여러 개의 카테고리에 속하는 것을 표현하고 있지만 근본적인 해결책이라고 할 수 없다.
따라서 M:N(다대다)을 해결하기 위해서 매핑(mapping) 테이블을 사용한다. 매핑 테이블은 두 테이블의 중간에서 필요한 정보를 양쪽에서 끌어서 쓰는 구조이다.
매핑 테이블의 특징은 다음과 같다.
[ 매핑(mapping) 테이블의 특징 ]
매핑 테이블의 작성 이전에 다른 테이블들이 먼저 존재해야 한다.
매핑 테이블은 주로 '명사'가 아닌 '동사'나 '히스토리'에 대한 데이터를 보관하는 용도이다.
매핑 테이블은 중간에서 양쪽의 PK를 참조하는 형태로 사용된다.
아래에서 진행할 예제의 주제로
'회원의 영화에 대해서 평점을 준다'를 구성한다고 가정하면
위와 같이 '평점을 준다'는 행위가 매핑 테이블에 필요한 부분이 된다.
[ JPA에서 M:N(다대다) 처리 ]
'영화(Movie)'와 '회원(Member)'이 존재하고 회원이 영화에 대한 평점과 감상을 기록하는 시나리오를 기반으로 구성
한 편의 영화는 여러 회원의 평가가 행해질 수 있다.
한 명의 회원은 여러 영화에 대해서 평점을 줄 수 있다.
예제를 위해 영화를 100개 삽입하고, 각 영화에 1~5개 사이의 랜덤 숫자로 이미지를 넣었다. 또한 100명의 회원을 넣고 무작위로 200개의 리뷰를 넣었다.
[ 페이지 처리되는 영화별 평균 점수/리뷰 개수 구하기 ]
영화의 목록 화면에서 영화와 영화 이미지, 리뷰의 수, 평점 평균을 화면에 출력하고자 한다.
현재 테이블 관계로 보면 영화와 영화 이미지는 '일대다'의 관계가 된다.
그리고 리뷰를 같이 조인하면 아래와 같은 구조가 된다.
우선, 영화와 리뷰를 이용해서 페이징 처리를 하면 다음과 같이 구성할 수 있다.
public interface MovieRepository extends JpaRepository<Movie, Long> {
@Query("select m, avg(coalesce(r.grade,0)), count(distinct r) from Movie m " +
"left outer join Review r on r.movie = m group by m")
Page<Object[]> getListPage(Pageable pageable);
}
테스트를 해본다.
@Test
public void testListPage(){
PageRequest pageRequest = PageRequest.of(0,10, Sort.by(Sort.Direction.DESC, "mno"));
Page<Object[]> result = movieRepository.getListPage(pageRequest);
for (Object[] objects : result.getContent()){
System.out.println(Arrays.toString(objects));
}
}
위와 같이 나오게 된다.
이와 같은 방식으로 중간에 영화 이미지도 같이 결합하면 될 것이라고 예상하고 코드를 수정한다.
public interface MovieRepository extends JpaRepository<Movie, Long> {
@Query("select m, max(mi), avg(coalesce(r.grade,0)), count(distinct r) from Movie m " +
"left outer join MovieImage mi on mi.movie = m " +
"left outer join Review r on r.movie = m group by m")
Page<Object[]> getListPage(Pageable pageable);
}
수정 된 코드를 실행한다.
먼저 페이지에 해당하는 데이터들을 가져온 뒤,
위의 쿼리가 10번 실행된다
쿼리의 내용은 movie_image 테이블에서 해당하는 모든 영화의 이미지를 다 가져오는 쿼리이다.
이유는 목록을 가져오는 쿼리는 문제가 없지만, max()를 사용하면서 해당 영화의 모든 이미지를 가져오는 쿼리가 실행된것이다. ( N + 1 문제 )
[ N + 1 문제 ]
1번의 쿼리로 N개의 데이터를 가져왔는데 N개의 데이터를 처리하기 위해서 필요한 추가적인 쿼리가 각 N개에 대해서 수행되는 상황을 의미한다.
위의 경우 1페이지에 해당하는 10개의 데이터를 가져오는 쿼리 1번과 각 영화의 모든 이미지를 가져오기 위한 10번의 추가적인 쿼리가 실행되는 것이다. 이렇게 되면 한 페이지를 볼 때 마다 11번의 쿼리를 실행하기 때문에 성능에 문제가 생기게 된다.
위 문제를 해결할 간단한 방법은 중간의 이미지를 1개로 줄여서 처리하는 것이다.
JPQL은 별도의 처리 없이 위의 구조를 작성할 수 있다.
public interface MovieRepository extends JpaRepository<Movie, Long> {
@Query("select m, mi, avg(coalesce(r.grade,0)), count(distinct r) from Movie m " +
"left outer join MovieImage mi on mi.movie = m " +
"left outer join Review r on r.movie = m group by m")
Page<Object[]> getListPage(Pageable pageable);
}
max() 처리를 없애주었다. 변경된 코드를 실행하면 hibernate 에서 반복적으로 실행 되는 부분이 없어진 것을 확인할 수 있다.
페이지에 해당하는 데이터들을 가져 온뒤, 갯수를 가져오는 쿼리만 실행되는 모습을 확인할 수 있다.
그렇다면 위의 JPQL 쿼리문에서
@Query("select m, mi, avg(coalesce(r.grade,0)), count(distinct r) from Movie m " +
"left outer join MovieImage mi on mi.movie = m " +
"left outer join Review r on r.movie = m group by m")
mi가 각 영화마다 연결된 1~5개 사이 랜덤숫자의 MovieImage 중 정확히 어떤것을 가져오는지 궁금해질것 이다.
이에 대한 해답은, 데이터베이스를 통해서 살펴보면 가장 낮은 번호로 연결된 것을 볼 수 있다.
'코드로 배우는 스프링부트 웹 프로젝트' 카테고리의 다른 글
Springboot) @RestController, @RequestMapping, @GetMapping 예제를 통한 학습 (0) | 2022.01.10 |
---|---|
Springboot) JPA에서 M:N(다대다) 처리 (2) , @EntityGraph (0) | 2022.01.07 |
Springboot) @Controller와 @RestController의 차이 (0) | 2021.12.28 |
Springboot) 관계형 DB 설계를 통한 Lazy loding/Eager loding 차이 , @ToString ,JPQL을 이용한 join (0) | 2021.12.13 |
Springboot) 방명록의 조회,수정,삭제 처리 (0) | 2021.12.09 |