본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 영화 리뷰 프로젝트] 1. M:N (다대다) 관계 설계와 구현 [+ N+1 문제와 엔티티의 특정 속성 로딩 방법]

반응형

목차

  1. 데이터베이스의 관계
    1. 엔티티 관계
    2. M:N 관계의 특징
    3. 매핑 테이블의 특징
  2. JPA의 연관관계 설정과 처리
    1. JPA에서 M:N 다대다 처리 방식
    2. @ManyToMany를 이용한 처리의 문제
  3. 연관관계가 있는 엔티티의 DTO 처리
    1. 엔티티 클래스 설계
  4. REST방식의 데이터 처리

 


엔티티 관계

  1. 영화와 회원 엔티티
  2. 회원이 영화에 대한 평점과 감상을 기록
    1. 한 편의 영화에 여러 회원의 평가
    2. 한 명의 회원은 여러 영화에 평점
      • 게시물과 댓글의 경우엔, 하나의 댓글은 하나의 게시물에만 속하지만
      • 영화와 회원은 각각 독립적인 존재

 

M:N 관계의 특징

  • 논리적 설계와 실제 테이블 설계가 다르다.
  • 개념적으로 다대다를 사용하지만, 실제 테이블 설계에서는 불가능하다.
    • 테이블 구조의 RDBMS에서는, 칼럼을 지정할 때 크기를 지정
    • 따라서, 칼럼의 개수를 늘리는 수평적 확장이 불가능
    • 튜플의 개수를 늘리는 수직적 확장이 가능한 점을 이용해, 연결 테이블로 다대다 관계를 구현
    • 매핑 테이블이라고도 불리는 연결 테이블은 두 테이블에서 필요한 정보들을 가져오는 구조

 

매핑 테이블의 특징

  • 다른 테이블의 정보를 이용하므로, 이전 테이블 정보가 존재해야 한다.
  • '동사'나 '히스토리'에 대한 데이터를 보관하는 용도
  • 양쪽의 PK를 참조하는 형태

 


JPA에서 M:N 다대다 처리 방식

  • @ManyToMany를 이용한 처리
  • 별도 엔티티를 설계하고, @ManyToOne을 이용한 처리

 

@ManyToMany를 이용한 처리의 문제

  • 각 엔티티와의 매핑 테이블이 자동으로 생성되는 방식
  • 새로운 필드를 만드는 게 불가능한 문제점
    • 리뷰테이블에서 필요한 영화의 평점 정보를 생성 불가
  • 양방향 참조를 이용하는 어노테이션으로 발생가능한 문제점
    • 영속성 컨텍스트에 존재하는 엔티티 객체의 상태와 DB의 상태를 동기화시켜야 한다.
    • 하나의 객체 수정시 다른 객체 상태 일치시키는 작업의 어려움
    • 따라서 실무에서는 단반향 참조 위주로 프로젝트 설계

진행 순서

  1. 매핑 테이블 직접 설계
  2. 직접 매핑 관계 연결
  3. 연관 관계를 이용한 조회가 필요한 데이터는 JPQL을 이용해 추출

1. 매핑 테이블 직접 설계

1-1. 엔티티 클래스 설계

 

(1) entity패키지에 날짜/시간 클래스인 BaseEntity 클래스 추가

package com.movie.boot4.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Getter
abstract class BaseEntity {

    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name ="moddate")
    private LocalDateTime modDate;

}

-> 리스너 사용을 위해 appplication에 @EnableJpaAuditing 어노테이션 추가

 

(2) Movie 엔티티 설계

  • M:N 관계 처리
    1. 명사에 해당하는 클래스 먼저 설계 -> 영화와 회원 엔티티 클래스 설계
    2. 매핑 테이블 설계는 마지막 단계에서 수행
  • 영화 Movie 엔티티, 영화 이미지 MovieImage 엔티티
    • Movie 엔티티의 경우 BaseEntity클래스를 상속해서 작성

 

Movie 엔티티

package com.movie.boot4.entity;

import com.fasterxml.jackson.databind.ser.Serializers;
import lombok.*;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Movie extends Serializers.Base {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;

    private String title;
}

 

MovieImage 엔티티

package com.movie.boot4.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString(exclude = "movie")
//연관 관계가 존재할 경우에, ToString을 사용할 때에 반복 주의
public class MovieImage {
    //값 객체가 아닌 엔티티객체로 설정하는 이유는 페이지 처리나 조인 처리가 많으므로
    //단방향 참조로 처리해 @Query를 통해 조인을 사용하므로, JPQL에서 엔티티일 경우 사용이 자유롭다.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long inum;

    //uuid클래스를 이용해 고유한 번호를 생성해 사용
    private String uuid;

    private String imgName;

    //이미지 저장경로는 년/월/일 구조를 의미한다.
    private String path;

    //movie 테이블이 PK를 가지며, movieimage테이블이 FK를 가지는 구조
    //다대일의 경우 지연로딩 설정 필요
    @ManyToOne(fetch = FetchType.LAZY)
    private Movie movie;
}

 

 

 

(3) Member 엔티티 설계

: 고유한 번호, 이메일, 아이디와 패스워드, 닉네임을 가진 클래스

 

BaseEntity를 상속하는 Member 엔티티

package com.movie.boot4.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Table(name = "m_member")
public class Member extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mid;

    private String email;

    private String pw;

    private String nickname;
    
}

 

 


1-2. 매핑 테이블을 위한 Review 클래스 설계

: 회원이 영화에 대해서 평점을 준다.

 

-> 평점을 준다는 행위에 필요한 매핑 테이블

: 두 테이블 사이에서 양쪽의 PK를 참조하는 구조로, 단방향 참조의 경우에 FK테이블 기준으로 설계

 

Review 엔티티

package com.movie.boot4.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
//연관 관계의 엔티티 사용하지 않도록 설정
@ToString(exclude = {"movie", "member"})
public class Review extends BaseEntity{

    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private Long reviewnum;

    //다대일 관계에서는 항상 기본값이 즉시 로딩이므로, 지연 로딩으로 설정하는 작업 필요
    @ManyToOne (fetch = FetchType.LAZY)
    private Movie movie;

    @ManyToOne (fetch = FetchType.LAZY)
    private Member member;

    private int grade;

    private String text;
}

 

 


1-3. M:N (다대다) Repository와 테스트

: 리포지토리 구성 후, 필요한 데이터 구하는 테스트 코드 수행

 

  • MovieRepository / MovieImageRepository
  • MovieRepositoryTests
  • MemberRepository
  • MemberRepositoryTests

 

 

(1) Repository 작성과 테스트 데이터 추가

:repository 패키지 작성 후, MovieRepository와 MovieImageRepository 인터페이스 구성

 

MovieRepository 인터페이스

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MovieRepository extends JpaRepository<Movie, Long> {
}

MovieImageRepository 인터페이스

package com.movie.boot4.repository;

import com.movie.boot4.entity.MovieImage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MovieImageRepository extends JpaRepository<MovieImage, Long> {
}

MovieRepositoryTests

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;
import java.util.stream.IntStream;

@SpringBootTest
public class MovieRepositoryTests {

    //테스트를 위한 의존성 주입
    @Autowired
    private MovieRepository movieRepository;
    @Autowired
    private MovieImageRepository movieImageRepository;

    //영화와 영화 이미지는 같은 시점에 삽입되어야 하므로
    //(1) Movie 객체를 먼저 save
    //(2) Movie 객체에 해당하는 PK를 이용해 영화 이미지들을 추가
    //(3) 영화 이미지는 최대 5개까지 임의로 저장 [특정 영화 이미지는 많을 수 있으므로 임의의 수로 처리]
    //트랜잭션이 끝나면 처리하도록 해야 하고, 커밋을 수행하도록 테스트 작성
    @Commit
    @Transactional
    @Test
    public void insertMovies(){
        //스트림과 빌더패턴을 이용한 영화 객체 생성
        IntStream.rangeClosed(1,100).forEach(i -> {
            //100까지 포함
            Movie movie = Movie.builder()
                    .title("Movie....."+i)
                    .build();
            System.out.println("-------------------");

            movieRepository.save(movie);
            
            int count = (int) (Math.random() *5)+1;
            //1,2,3,4
            
            for(int j=0;j<count;j++){
                MovieImage movieImage=MovieImage.builder()
                        //UUID 클래스를 이용한 고유한 번호를 만들고 -> toString
                        .uuid(UUID.randomUUID().toString())
                        .movie(movie)
                        .imgName("test"+j+".jpg")
                        .build();
                movieImageRepository.save(movieImage);
                System.out.println("====================================");
            }

        });
    }
}

 

MemberRepository

package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class MemberRepositoryTests {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    public void insertMembers(){
        IntStream.rangeClosed(1,100).forEach(i->{
            Member member= Member.builder()
                    .email("r"+i+"@naver.com")
                    .pw("1234")
                    .nickname("reviewer"+i)
                    .build();
            memberRepository.save(member);
        });
    }
}

 

ReviewRepository

package com.movie.boot4.repository;

import com.movie.boot4.entity.Review;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReviewRepository extends JpaRepository<Review, Long> {
}

 

ReviewRepositoryTests

package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class ReviewRepositoryTests {
    @Autowired
    private ReviewRepository reviewRepository;
    
    @Test
    public void insertMovieReviews(){

        IntStream.rangeClosed(1,200).forEach(i->{
            
            //영화 번호에 해당하는 영화에 리뷰어 번호에 해당하는 리뷰어가 리뷰를 작성해야하므로
            //영화번호, 리뷰어번호, 리뷰번호
            //-> 멤버 객체, 영화 객체, 리뷰 객체 생성해 저장
            Long mno=(long)(Math.random() *100)+1;
            
            Long mid=((long)(Math.random()*100)+1);
            
            
            //멤버 객체
            Member member =Member.builder()
                    .mid(mid)
                    .build();

            //리뷰 객체
            Review movieReview=Review.builder()
                    .member(member)
                    .movie(Movie.builder().mno(mno).build())
                    .grade((int)(Math.random()*5)+1)
                    .text("이 영화에 대한 느낌..."+i)
                    .build();
            
            reviewRepository.save(movieReview);
            
        });
        
    }
}

화면 출력을 위한 진행 순서

  1. 화면에 필요한 데이터 처리
    1. 목록 화면에서 영화의 제목과 이미지 하나, 영화 리뷰의 평점/리뷰 개수 출력
    2. 영화 조회화면에서 영화와 영화 이미지들, 리뷰의 평균점수/리뷰 개수를 같이 출력
    3. 리뷰에 대한 정보에는 회원의 이메일이나 닉네임과 같은 정보를 같이 출력
  2. 데이터 처리 방식은 @Query를 이용한 JPQL로 처리
    • @EntityGraph나 서브쿼리를 활용
  3. 일대다 관계로 놓인 영화와 영화 이미지 엔티티에서 리뷰를 조인

 

진행 순서

  1. 페이지 처리되는 영화별 평균 점수/리뷰 개수
  2. 특정 영화의 모든 이미지와 평균 평점/ 리뷰 개수

1. 페이지 처리되는 영화별 평균 점수/리뷰 개수 구하기

: JPQL에서 group by를 적용해, 리뷰의 개수와 리뷰의 평균 평점을 구한다.

 

(1) 영화와 리뷰를 이용해 페이징 처리

 

MovieRepository - 비오라클 사용시

: only full group by 사용

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

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);
}

 

MovieRepository - 오라클 사용시

: only full group by 사용 불가

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

public interface MovieRepository extends JpaRepository<Movie, Long> {

    //영화와 리뷰를 이용한  페이징 처리
    @Query("select min(r.movie), avg(NVL(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);
}

 

+) MovieRepositoryTest의 testListPage에 @Transactional 추가 필요

: Review엔티티가 Movie객체를 지연로딩으로 참조하기 때문에, 프록시 객체를 못찾는 문제가 발생하기 때문

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.IntStream;

@SpringBootTest
public class MovieRepositoryTests {

    //테스트를 위한 의존성 주입
    @Autowired
    private MovieRepository movieRepository;
    @Autowired
    private MovieImageRepository movieImageRepository;
    
    //중략
    
    
    @Transactional
    @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));
        }
    }
}

 

(2) 페이징 처리 후, 영화 이미지를 추가

 

영화마다 여러개의 이미지가 존재하는 구조

 

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

public interface MovieRepository extends JpaRepository<Movie, Long> {

    //영화와 리뷰를 이용한  페이징 처리
    //영화 기준
    @Query("select min(r.movie), max(mi), avg(NVL(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);
}

->모든 영화마다 조회를 하는 N+1문제 발생

: 영화 이미지 테이블에서 해당하는 모든 영화의 이미지를 다 가져오기 때문에, 비효율적인 SQL문

[max() 함수에서 문제 발생]

 

더보기

N+1 문제

:1번의 쿼리로 N개의 쿼리를 가져오는데 N개의 데이터를 처리하기 위해 필요한 추가적인 쿼리가 각 N개에 대해서 수행

 

1페이지에 해당하는 10개의 데이터를 가져오는 쿼리 1번과 각 영화의 모든 이미지를 가져오기 위한 10번의 추가적인 쿼리 실행

-> 한 페이지 출력시 11번의 쿼리를 실행하는 비효율적인 쿼리

 

(3) N+1문제 해결 : 영화마다의 이미지를 1개로 줄여서 처리하는 방법

한 영화에 해당하는 이미지를 하나로 선택하는 바람직한 데이터 구조

-> 문제가 되는 max()를 없애고, MovieImage를 출력한다.

 

: 중간에 반복적으로 실행되는 부분 없이, 목록을 구하는 쿼리와 개수를 구하는 쿼리만 실행

 

MovieRepository - 비오라클

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

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);
}

 

MovieRepository - 오라클

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

public interface MovieRepository extends JpaRepository<Movie, Long> {

    //영화와 리뷰를 이용한  페이징 처리
    //영화 기준
    @Query("select min(r.movie), min(mi), avg(NVL(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);
}

-> Movie와 MovieImage와의 관계는 가장 먼저 입력된 이미지 번호와 연결

더보기

가장 나중에 입력된 이미지 번호와 연결하는 방법

 @Query("select min(r.movie), min(mi), avg(NVL(r.grade, 0)), count (distinct r) from Movie m "
            //영화 이미지 외부 조인
            +"left outer join MovieImage mi on mi.movie =m "
            //나중에 입력된 이미지를 선택하는 쿼리
            +"and mi.inum=(select max(i2.inum) from MovieImage i2 where i2.movie=m)"
            //리뷰 외부 조인 + 영화 기준 그룹바이
            + "left outer join Review r on r.movie=m group by m")

2. 특정 영화의 모든 이미지와 평균 평점 / 리뷰 개수

: 영화 조회시 해당 영화의 평균 평점 / 리뷰 개수를 화면에서 사용하므로, MovieRepository에 해당 기능 추가

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

public interface MovieRepository extends JpaRepository<Movie, Long> {

    //영화와 리뷰를 이용한  페이징 처리
    //중략

    //특정 영화 조회 처리
    @Query("select mi.movie,mi from Movie m "
    +"left outer join MovieImage mi on mi.movie = m where mi.movie.mno = :mno")
    List<Object[]> getMovieWithAll(Long mno);
}

 

: 리뷰와 관련된 내용처리 : left join 이용

-> 리뷰와 조인 후, count(), avg()등의 함수를 이용하는데, 이 때 영화 이미지 별로 group by 실행

 

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

public interface MovieRepository extends JpaRepository<Movie, Long> {

    //영화와 리뷰를 이용한  페이징 처리
    //중략

    //특정 영화 조회 처리
    //: Group by를 이용해 영화 이미지별로 그룹을 생성해 이미지 개수만큼 데이터를 만든다.
    @Query("select min(mi.movie),min(mi), avg(NVL(r.grade, 0)), count(r) from Movie m "
            //리뷰 테이블과 외부조인
            +"left outer join Review r on r.movie =m "
            //영화이미지 테이블과 외부조인
            +"left outer join MovieImage mi on mi.movie = m where mi.movie.mno = :mno "
            +"group by mi ")
    List<Object[]> getMovieWithAll(Long mno);
}

: 영화 이미지별로 그룹을 만들어 이미지 개수만큼 데이터를 만든다.

 


3. 특정 영화의 모든 리뷰와 회원의 닉네임

: 영화 조회 화면 기능

  1. 영화에 대한 리뷰 조회
  2. 영화 리뷰 등록
  3. 영화 리뷰 수정/삭제

 

영화 조회 화면 기능을 구현하기 위해 필요한 데이터

  1. 특정한 영화의 번호
  2. 영화 번호를 이용한 해당 영화의 리뷰 정보
package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.stream.IntStream;

@SpringBootTest
public class ReviewRepositoryTests {
    @Autowired
    private ReviewRepository reviewRepository;

    @Test
    public void insertMovieReviews() {

        IntStream.rangeClosed(1, 200).forEach(i -> {

            //영화 번호에 해당하는 영화에 리뷰어 번호에 해당하는 리뷰어가 리뷰를 작성해야하므로
            //영화번호, 리뷰어번호, 리뷰번호
            //-> 멤버 객체, 영화 객체, 리뷰 객체 생성해 저장
            Long mno = (long) (Math.random() * 100) + 1;

            Long mid = ((long) (Math.random() * 100) + 1);


            //멤버 객체
            Member member = Member.builder()
                    .mid(mid)
                    .build();

            //리뷰 객체
            Review movieReview = Review.builder()
                    .member(member)
                    .movie(Movie.builder().mno(mno).build())
                    .grade((int) (Math.random() * 5) + 1)
                    .text("이 영화에 대한 느낌..." + i)
                    .build();

            reviewRepository.save(movieReview);

        });
    }

        //특정 영화의 모든 리뷰와 회원의 닉네임 조회 테스트
        @Test
        public void testGetMovieReviews(){
            //필요한 정보
            //(1) 영화 객체
            //(2) 찾은 영화의 리뷰 정보를 담은 리뷰 리스트

            Movie movie=Movie.builder().mno(92L).build();

            List<Review> result = reviewRepository.findByMovie(movie);

            //reuslt를 돌면서 리뷰 정보 출력하는 forEach문
            result.forEach(movieReview -> {
                System.out.println(movieReview.getReviewnum());
                System.out.println("\t"+movieReview.getGrade());
                System.out.println("\t"+movieReview.getText());
                System.out.println("\t"+movieReview.getMember());
                System.out.println("\t"+movieReview.getMember().getEmail());
                System.out.println("---------------------------");

            });
        }

}

-> 지연로딩으로 인한 한 번에 Review 객체와 Member 객체를 조회하지 못하는 문제

: @Transactional로 해결

 

: Rolled back transaction for test 즉, 예외로 인한 롤백처리


지연로딩으로 인한 문제를 @Transactional로 해결 못하는 경우

: 각 Review 객체에서 필요한 정보를 가져오기 위해 Member객체를 로딩해야하는 경우

-> 즉, 지연로딩이 두 번 걸리는 경우

 

객체를 여러개 조회할 때, 특정 데이터를 위해 객체를 로딩해야 할 경우

  1. @Query를 이용한 조인 처리
  2. @EntityGraph를 이용해 엔티티의 필요한 속성만 함께 로딩하는 방법
    • 한 객체 처리시 필요한 데이터를 처리하기 위한 객체 또한 로딩하도록 변경하는 방법

@EntityGraph 특징

  • attributePaths 속성과 type 속성
    1. attributePaths 속성
      • 로딩 설정을 변경하고 싶은 속성의 이름을 배열로 명시
    2. type 속성
      • @EntityGraph를 어떤 방식으로 적용할 것인지 설정
        1. FETCH : 명시한 속성을 EAGER로 처리, 나머지 LAZY로 처리
        2. LOAD   : 명시한 속성을 EAGER로 처리, 나머지 엔티티 클래스에 명시 OR 기본 방식으로 처리

@EntityGraph 적용한 ReviewRepository

package com.movie.boot4.repository;

import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ReviewRepository extends JpaRepository<Review, Long> {

    //특정 영화의 모든 리뷰와 회원의 닉네임
    //: 특정 객체의 엔티티 속성만을 먼저 로딩하는 방법
    @EntityGraph(attributePaths = {"member"}, type= EntityGraph.EntityGraphType.FETCH)
    List<Review> findByMovie(Movie movie);
}

 


4. 회원 삭제 문제와 트랜잭션 처리

: 다대다 관계에서 명사에 해당하는 데이터 삭제시 중간에 매핑 테이블에서의 삭제도 필요하다.

-> 특정 회원 삭제시, 해당 회원이 등록한 모든 영화 리뷰 또한 삭제해야 하기 때문에

 

1번 회원 삭제시

(1) 1번 회원이 작성한 리뷰 3개를 먼저 삭제

(2) 리뷰 삭제 후 회원 삭제 진행

(3) 이 두가지 작업은 하나의 트랜잭션으로 수행

-> ReviewRepository에 회원을 이용해 삭제하는 메서드 추가

 

ReviewRepository

    void deleteByMember(Member member);

 

package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import java.util.stream.IntStream;

@SpringBootTest
public class MemberRepositoryTests {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private ReviewRepository reviewRepository;
    @Test
    public void insertMembers(){
        IntStream.rangeClosed(1,100).forEach(i->{
            Member member= Member.builder()
                    .email("r"+i+"@naver.com")
                    .pw("1234")
                    .nickname("reviewer"+i)
                    .build();
            memberRepository.save(member);
        });
    }

    //회원 삭제시 리뷰 삭제후 회원 삭제하는 메서드 테스트

    @Test
    public void testDeleteMember(){
        //삭제할 멤버의 번호와 해당 멤버객체 생성
        Long mid = 1L;
        Member member = Member.builder()
                .mid(mid)
                .build();

        //FK위배의 경우
        memberRepository.deleteById(mid);
        reviewRepository.deleteByMember(member);


    }
}

문제점

(1) 참조 무결성 제약조건 위배

(2) 트랜잭션 조건 위배

 

해결

(1) @Transactional, @Commit 추가

(2) 한번에 삭제하기위한 쿼리로 변경

 

 

 

package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.Review;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ReviewRepository extends JpaRepository<Review, Long> {

    //특정 영화의 모든 리뷰와 회원의 닉네임
    //: 특정 객체의 엔티티 속성만을 먼저 로딩하는 방법
    @EntityGraph(attributePaths = {"member"}, type= EntityGraph.EntityGraphType.FETCH)
    List<Review> findByMovie(Movie movie);

    //회원 삭제시 리뷰 먼저 삭제 후 회원 삭제하는 메서드
    //: 비효율적인 쿼리를 개선하기 위한 어노테이션 필요 -> update/delete할 경우 필요
    //-> 해당 회원의 리뷰를 한번에 삭제하는 쿼리
    @Modifying
    @Query("delete from Review mr where mr.member=:member ")
    void deleteByMember(Member member);
}

 

MemberRepositoryTests

package com.movie.boot4.repository;

import com.movie.boot4.entity.Member;
import com.movie.boot4.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import java.util.stream.IntStream;

@SpringBootTest
public class MemberRepositoryTests {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private ReviewRepository reviewRepository;
    @Test
    public void insertMembers(){
        IntStream.rangeClosed(1,100).forEach(i->{
            Member member= Member.builder()
                    .email("r"+i+"@naver.com")
                    .pw("1234")
                    .nickname("reviewer"+i)
                    .build();
            memberRepository.save(member);
        });
    }

    //회원 삭제시 리뷰 삭제후 회원 삭제하는 메서드 테스트

    //리뷰가 존재할 때 회원 삭제 오류 발생
    //(1) 참조 무결성 제약조건 위배
    //-> FK를 가지는 Review 쪽을 먼저 삭제하지 않았기 때문에
    //(2) 트랜잭션 관련 처리 미비
    //-> 회원 삭제시 회원이 작성한 리뷰와 회원 삭제는 동시에 일어나야하는 조건
    @Transactional
    @Commit
    @Test
    public void testDeleteMember(){
        //삭제할 멤버의 번호와 해당 멤버객체 생성
        Long mid = 1L;
        Member member = Member.builder()
                .mid(mid)
                .build();

        //FK위배의 경우
        //memberRepository.deleteById(mid);
        //reviewRepository.deleteByMember(member);

        //@Transactional과 @Commit 추가 후에도
        // 비효율적인 SQL -> 하나씩 삭제하기 위해 리뷰 개수만큼 반복 실행
        //: @Query를 이용해 where절 지정
        reviewRepository.deleteByMember(member);
        memberRepository.deleteById(mid);

    }
}

 

 

 

 


인텔리제이 콘솔창 한글 깨짐

 

https://www.lesstif.com/java/intellij-file-console-encoding-121012310.html

 

IntelliJ 한글 깨짐 문제 해결(file 과 console encoding 설정)

 

www.lesstif.com

 

반응형