Day_37 ( 스프링 - 4 / Spring Data JPA )

HD.Y·2023년 12월 19일
0

한화시스템 BEYOND SW

목록 보기
32/58
post-thumbnail

오늘의 학습 내용

  • 즉시 로딩과 지연 로딩이란❓

    1) 즉시 로딩 : 필요하지 않은 속성까지도 한번에 조회하는 것
      ➡ 설정법 : @[관계 / ex) OnetoMany](fetch = fetch.EAGER)

    2) 지연 로딩 : 필요한 것만 조회하고 필요하지 않은 것은 나중에 필요할 때
      다시 조회하는 것
      ➡ 설정법 : @[관계 / ex) OnetoMany](fetch = fetch.LAZY)

  • 아래에서 예를 들어 설명해보겠다.
    Member 엔티티와 Team 엔티티가 N:1 관계로 형성되어 있고, 안에는 데이터를 미리 채워놨다.

    Member 테이블


    Team 테이블


  • 여기서 Member 엔티티에 즉시로딩을 설정해준 뒤 Member 테이블에 id와 username만 조회해본다고 하자.

     // Member 클래스에서 작성
      @ManyToOne(fetch = FetchType.EAGER)
      @JoinColumn(name = "Team_id")
      private Team4 team;
      
      // Main 클래스
      public class Main {
        public static void main(String[] args) {
           EntityManagerFactory emf =
                   Persistence.createEntityManagerFactory(
                           "hibernateproject");
           EntityManager em = emf.createEntityManager();
           EntityTransaction tx = em.getTransaction();
    
           tx.begin();
    
           Member4 member4 = em.find(Member4.class, 1);
           System.out.println(member4.getId());
           System.out.println(member4.getUsername());
         
           tx.commit();
         }
      }

  • 이랬을때, 즉시로딩은 단순히 Member 테이블에서 idusername 만 가져오는데도 Team 테이블과 LEFT JOIN 을 아래와 같이 자동으로 실시한다.

  • 이처럼 즉시 로딩은 내가 필요한 데이터가 굳이 테이블 조인을 하지 않아도 조회할 수 있지만 조인을 해버린다.


  • 반대로 이제는 지연로딩을 설정해준 뒤 똑같이 Member 테이블에서 idusername을 조회해보고 이어서 Team_id 까지 조회해본다고 하자.

     // Member 클래스에서 작성
      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = "Team_id")
      private Team4 team;
      
      // Main 클래스
      public class Main {
        public static void main(String[] args) {
           EntityManagerFactory emf =
                   Persistence.createEntityManagerFactory(
                           "hibernateproject");
           EntityManager em = emf.createEntityManager();
           EntityTransaction tx = em.getTransaction();
    
           tx.begin();
    
           Member4 member4 = em.find(Member4.class, 1);
           System.out.println(member4.getId());
           System.out.println(member4.getUsername());
           System.out.println(member4.getTeam().getName());
         
           tx.commit();
         }
      }

  • 위처럼 지연 로딩을 걸어주면 테이블 조인을 하지 않고, 먼저 Member 테이블에서 idusername을 조회하고 이어서 다시 한번더 Team_id를 조회하는 것을 아래와 같이 볼 수 있을 것이다.

  • 지연 로딩은 이렇게 먼저 필요한 것만 조회하고 필요하지 않은 것은 나중에 필요해지면 다시 조회한다.

    이때 그럼 나중에 필요한 데이터를 어디에 저장해놓고 가져오는 것일까❓라는 의문을 품을 수 있다.

  • 그것이 바로 프록시 객체이다. 위의 예를 들었을때 처음에 필요한 것은 Member 테이블의 idusername 이 필요해서 DB에서 찾아서 1차 캐시에 저장을 한다.
    이때, Team_id는 조회를 하지 않았기 때문에 1차 캐시에 저장이 안되는데, 나중에 필요할 수 있는 것이다.

    따라서 이러한 데이터를 프록시 객체에 저장시켜놨다가 나중에 필요해서 조회할때 이 프록시 객체에서 저장되어 있는 데이터를 불러오게 되는 것이다.


🐯 지연로딩과 즉시로딩 실습해보기

  • User(회원) 엔티티에는 id, pw, name 이 있다.
  • Order(주문) 엔티티에는 id, productName 이 있다.
  • Grade(회원 등급) 엔티티에는 id, grade 가 있다.

  • 회원과 회원등급은 N:1 관계이다.
  • 회원과 주문은 1:N 관계이다.

  • 회원의 정보를 조회하면 회원등급까지 같이 조회된다. ( 즉시 로딩 )
  • 회원의 정보를 조회하고, 필요 시 해당회원의 주문정보를 조회한다. ( 지연 로딩 )

User 엔티티

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String pw;
    private String name;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "User_id")
    private List<Order> orderList = new ArrayList<>();

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "Grade_id")
    private Grade grade;
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getPw() {
        return pw;
    }

    public void setPw(String pw) {
        this.pw = pw;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Order> getOrderList() {
        return orderList;
    }

    public void setOrderList(List<Order> orderList) {
        this.orderList = orderList;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

Order 엔티티

@Entity
@Table(name="`Order`")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String productName;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }
}

Grade 엔티티

@Entity
public class Grade {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String grade;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }
}

  • 각각의 테이블에 데이터를 미리 넣어논 상태에서 아래와 같이 Main 클래스를 작성하여 실행해보겠다.
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory(
                        "hibernateproject");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

// 데이터 삽입 코드
//        for (int i = 0; i < 5; i++) {
//            Grade grade = new Grade();
//            grade.setGrade("등급"+(i+1));
//            for (int j = 1; j <= 10; j++) {
//                User user = new User();
//                user.setName("회원"+(i*10+j));
//                user.setPw("비밀번호" + (i*10+j));
//                user.setGrade(grade);
//                em.persist(user);
//
//                for (int k=1; k<=3; k++) {
//                    Order order = new Order();
//                    order.setProductName("상품" + (i*10+k));
//                    em.persist(order);
//                    user.getOrderList().add(order);
//                }
//            }
//            em.persist(grade);
//        }

        User user1 = em.find(User.class, 30);
        System.out.println("회원명 : " + user1.getName());
        System.out.println("비밀번호 : " + user1.getPw());
        System.out.println("등급 : " + user1.getGrade().getGrade());

        User user2 = em.find(User.class, 2);
        System.out.println("회원명 : " + user2.getName());
        System.out.println("비밀번호 : " + user2.getPw());
        System.out.println("등급 : " + user2.getGrade().getGrade());

        System.out.print("주문목록 : ");
        for(Order order : user2.getOrderList()) {
            System.out.print(order.getProductName() + " ");
        }

        tx.commit();
    }
}
  • 실행했을때, 회원과 회원등급은 즉시 로딩으로 설정했기 때문에 아래와 같이 LEFT JOIN 하여 쿼리문이 1번 실행하여 결과가 조회된다.

  • 하지만, 회원과 주문목록은 지연 로딩으로 설정했기 때문에 아래와 같이 두개의 테이블이 JOIN 되는 것이 아니라 쿼리문이 2번 실행되어 결과가 조회되는 것을 볼 수 있었다.
    업로드중..

🐷 Spring Data JPA 실습하기

  • 이제까지 JPA에 대한 이론과 실습을 진행하였고, 아래부터는 Spring Data JPA 에 대한 실습을 진행하겠다.

  • Spring Data JPA 에는 JPA 와 마찬가지로 Entity(테이블과 직접적으로 매핑되는 클래스)가 있고 이전에 JDBC로 실습할때 생성하였던 DAO 클래스 대신에 Entity 객체들을 처리하는 기능을 가진 Repository 인터페이스가 있다.

  • Spring Data JPA 를 사용하기 위해서는 pom.xml 에 아래의 라이브러리를 추가해주고, application.yml 파일에도 설정을 추가해줘야된다.

    // ✅ pom.xml에 작성
    <dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-jpa</artifactId>
     </dependency>
     
     // ✅ application.yml에 작성 ( 들여쓰기 매우 중요!! )
     spring:
      jpa:
        hibernate:
         ddl-auto: update
        properties:
         hibernate:
          format_sql: true
        show-sql: true
  • 본격적인 Spring Data JPA 실습 내용

    1) 영화( 영화 이름, 가격 ) CRUD 구현

    2) 영화에 대한 리뷰 기능( 별점, 내용 ) CRUD 구현

    3) 영화와 리뷰는 1:N 관계로 양방향 설정

    ➡ 작성은 DB와 가까운 순으로 작성하는 것이 좋다.
      따라서 Repository - DTO(전송 객체) - Service - Controller 순으로 작성하면 된다.

  • 먼저 영화에 대한 CRUD 구현 결과이다.

// ✅ MovieRepository 인터페이스
@Repository
public interface MovieRepository extends JpaRepository<Movie, Integer> {
}

// ✅ MovieDto 클래스
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MovieDto {
    private Integer id;
    private String name;
    private Integer price;

    private List<MovieReviewDto> movieReviewDtoList = new ArrayList<>();
}

// ✅ MovieService 클래스
@Service
public class MovieService {
    MovieRepository movieRepository;

    public MovieService(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }
	
    // 💻 CREATE 기능
    public void create(MovieDto movieDto) {

        movieRepository.save(Movie.builder()
                .name(movieDto.getName())
                .price(movieDto.getPrice())
                .build());
    }
	
    // 💻 LIST 기능 (전체 목록 조회)
    public List<MovieDto> list() {
        List<Movie> result = movieRepository.findAll();

        List<MovieDto> movieDtos = new ArrayList<>();
        for(Movie movie : result) {
            List<MovieReviewDto> movieReviewDtos = new ArrayList<>();
            List<MovieReview> movieReviewList = movie.getMovieReviewList();

            for(MovieReview movieReview : movieReviewList) {
                MovieReviewDto movieReviewDto = MovieReviewDto.builder()
                        .id(movieReview.getId())
                        .star(movieReview.getStar())
                        .text(movieReview.getText())
                        .build();

                movieReviewDtos.add(movieReviewDto);
            }

            MovieDto movieDto = MovieDto.builder()
                    .id(movie.getId())
                    .name(movie.getName())
                    .price(movie.getPrice())
                    .movieReviewDtoList(movieReviewDtos)
                    .build();
            movieDtos.add(movieDto);
        }

        return movieDtos;
    }

    // 💻 READ 기능 (한개만 조회)
    public MovieDto read(Integer id) {
        Optional<Movie> result = movieRepository.findById(id);
        if (result.isPresent()) {
            Movie movie = result.get();

            List<MovieReviewDto> movieReviewDtos = new ArrayList<>();

            for(MovieReview movieReview : movie.getMovieReviewList()) {
                movieReviewDtos.add(MovieReviewDto.builder()
                        .id(movieReview.getId())
                        .star(movieReview.getStar())
                        .text(movieReview.getText())
                        .build()
                );
            }

            return MovieDto.builder()
                    .id(movie.getId())
                    .price(movie.getPrice())
                    .name(movie.getName())
                    .movieReviewDtoList(movieReviewDtos)
                    .build();
        } else {
            return null;
        }
    }
	
    // 💻 UPDATE 기능
    public void update(MovieDto movieDto) {
        movieRepository.save(Movie.builder()
                .id(movieDto.getId())
                .name(movieDto.getName())
                .price(movieDto.getPrice())
                .build());
    }
	
    // 💻 DELETE 기능
    public void delete(Integer id) {
        movieRepository.delete(Movie.builder().id(id).build());
    }
}

// ✅ MovieController 클래스
@RestController
@RequestMapping("/movie")
public class MovieController {
    MovieService movieService;

    public MovieController(MovieService movieService) {
        this.movieService = movieService;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/create")
    public ResponseEntity create(MovieDto movieDto) {
        movieService.create(movieDto);

        return ResponseEntity.ok().body("생성");
    }

    @RequestMapping(method = RequestMethod.GET, value = "/list")
    public ResponseEntity list() {

        return ResponseEntity.ok().body(movieService.list());
    }

    @RequestMapping(method = RequestMethod.GET, value = "/read")
    public ResponseEntity read(Integer id) {

        return ResponseEntity.ok().body(movieService.read(id));
    }

    @RequestMapping(method = RequestMethod.PATCH, value = "/update")
    public ResponseEntity update(MovieDto movieDto) {
        movieService.update(movieDto);

        return ResponseEntity.ok().body("수정");
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "/delete")
    public ResponseEntity delete(Integer id) {
        movieService.delete(id);
        return ResponseEntity.ok().body("삭제");

    }
}

  • 아래부터는 리뷰에 대한 CRUD 를 구현 결과이다.
// ✅ MovieReviewRepository 인터페이스
@Repository
public interface MovieReviewRepository extends JpaRepository<MovieReview, Integer> {
}

// ✅ MovieReviewDto 클래스
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MovieReviewDto {
    private Integer id;
    private Integer star;
    private String text;

    private MovieDto movieDto;
}

// ✅ MovieReviewService 클래스
@Service
public class MovieReviewService {


    MovieReviewRepository movieReviewRepository;

    public MovieReviewService(MovieReviewRepository movieReviewRepository) {
        this.movieReviewRepository = movieReviewRepository;
    }

    // 💻 CREATE 기능
    public void create(Integer movieid, MovieReviewDto movieReviewDto) {
        movieReviewRepository.save(MovieReview.builder()
                        .movie(Movie.builder().id(movieid).build())
                        .star(movieReviewDto.getStar())
                        .text(movieReviewDto.getText())
                        .build());
    }

    // 💻 LIST 기능 (전체 목록 조회)
    public List<MovieReviewDto> list() {
        List<MovieReview> result = movieReviewRepository.findAll();

        List<MovieReviewDto> movieReviewDtos = new ArrayList<>();

        for (MovieReview movieReview:result) {
            Movie movie = movieReview.getMovie();
            MovieDto movieDto = MovieDto.builder()
                    .id(movie.getId())
                    .name(movie.getName())
                    .price(movie.getPrice())
                    .build();

            MovieReviewDto movieReviewDto = MovieReviewDto.builder()
                    .id(movieReview.getId())
                    .star(movieReview.getStar())
                    .text(movieReview.getText())
                    .movieDto(movieDto)
                    .build();
            movieReviewDtos.add(movieReviewDto);
        }

        return movieReviewDtos;
    }

    // 💻 READ 기능 (한개만 조회)
    public MovieReviewDto read(Integer id) {
        Optional<MovieReview> result = movieReviewRepository.findById(id);
        if(result.isPresent()) {
            MovieReview movieReview = result.get();

            return MovieReviewDto.builder()
                    .id(movieReview.getId())
                    .star(movieReview.getStar())
                    .text(movieReview.getText())
                    .movieDto(MovieDto.builder()
                            .id(movieReview.getMovie().getId())
                            .name(movieReview.getMovie().getName())
                            .price(movieReview.getMovie().getPrice())
                            .build())
                    .build();
        } else {
            return null;
        }
    }

    // 💻 UPDATE 기능
    public void update(MovieReviewDto movieReviewDto) {
        Optional<MovieReview> result = movieReviewRepository.findById(movieReviewDto.getId());

        if(result.isPresent()) {
            MovieReview movieReview = result.get();
            movieReview.setStar(movieReviewDto.getStar());
            movieReview.setText(movieReviewDto.getText());
            movieReviewRepository.save(movieReview);
        }
    }

    // 💻 DELETE 기능
    public void delete(Integer id) {
        movieReviewRepository.delete(MovieReview.builder().id(id).build());
    }
}

// ✅ MovieReviewController 클래스
@RestController
@RequestMapping("/movie_review")
public class MovieReviewController {

    MovieReviewService movieReviewService;

    public MovieReviewController(MovieReviewService movieReviewService) {
        this.movieReviewService = movieReviewService;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/create")
    public ResponseEntity create(Integer movieid, MovieReviewDto movieReviewDto) {
        movieReviewService.create(movieid, movieReviewDto);

        return ResponseEntity.ok().body("리뷰 작성 성공");
    }

    @RequestMapping(method = RequestMethod.GET, value = "/list")
    public ResponseEntity list() {
        return ResponseEntity.ok().body(movieReviewService.list());
    }

    @RequestMapping(method = RequestMethod.GET, value = "/read")
    public ResponseEntity read(Integer id) {
        return ResponseEntity.ok().body(movieReviewService.read(id));
    }

    @RequestMapping(method = RequestMethod.PATCH, value = "/update")
    public ResponseEntity update(MovieReviewDto movieReviewDto) {
        movieReviewService.update(movieReviewDto);

        return ResponseEntity.ok().body("리뷰 수정 성공");
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "/delete")
    public ResponseEntity delete(Integer id) {
        movieReviewService.delete(id);
        return ResponseEntity.ok().body("리뷰 삭제 성공");
    }
}

오늘의 느낀점 👀

  • 오늘은 Spring Data JPA를 본격적으로 배워본 시간이었다. 먼저 얘기하고 싶은건 위의 코드를 구현하는것에서 가장 어려웠던것은 조회(LIST, READ) 였다. 실제로 서비스를 운영하는 기업의 DB에서 가장 많이 일어날 수 있는 것이 조회인데, 그래서 그런지 유독 구현하는것도 복잡했다. 하지만 처음이라 그런거라고 생각한다. 수업 들을때는 어려웠지만, 집에 와서 복습해보면서 다시 한번 코드를 짜보니 이제는 왜 그렇게 짜는지 눈에 보이기 시작했다.

  • 사실 코드는 몇번이고 반복해가면서 익숙해질때까지 백지 상태에서 완벽히 구현할때까지 반복하면 익숙해질 것이다. 그보다 중요한건 개념이라고 생각한다. 어제부터 오늘까지 배운 ORM 그리고 JPA 에 대한 명확한 개념을 숙지하는것이 나에게 가장 필요한 부분이 아닐까 싶다.

  • 특히, 여기서 발생할 수 있는 문제 중에 대표적으로 n+1 문제 가 있다고 하는데 이것은 내일 수업때 다뤄볼 예정이다. 이부분까지 포함하여 JPA 에 대한 완벽한 개념을 숙지한다면 한단계 더 성장한 내가 될 것이다.

profile
Backend Developer

0개의 댓글