SpringBoot with JPA 프로젝트(N:1) 2.@ManyToOne,Lazy loading

mingki·2022년 2월 8일
1

SpringBoot & JPA

목록 보기
12/26


📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/board2022

1.@ManyToOne 어노테이션

JPA에서 관계를 고민할 때는 FK쪽을 먼저 해석하면 편리하다 앞에서 만든 Board 와 Member의 관계는 N:1(다대일) 관계가 되므로
@ManyToOne 어노테이션을 적용해야 한다

  • @ManyToOne : DB상에서 외래키의 관계로 연결된 엔티티 클래스에 설정한다

1-1.Board Entity 클래스의 연관관계 설정

Board 클래스는 작성자가 Member 엔티티를 의미하므로 아래와 같이 참조하는 부분을 추가한다

import lombok.*;
import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "writer") // exclude : toString 대상에서 제외한다
public class Board extends BaseEntity{

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

    private String title;

    private String content;

    @ManyToOne // 연관관계 지정
    private Member writer;

}

1-2.Reply Entity 클래스의 연관관계 설정

Reply 클래스는 Board 의 PK를 참조한다


import lombok.*;
import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "board")
public class Reply extends BaseEntity{

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

    private String text;

    private String replyer;

    @ManyToOne
    private Board board; // 연관관계 지정
}

2. Repository 인터페이스 추가

2-1.BoardRepository 생성

import com.example.board2022.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board,Long> {


}

2-2.MemberRepository 생성


import com.example.board2022.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member,String> {
}

2-3.ReplyRepository 생성


import com.example.board2022.entity.Reply;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReplyRepository extends JpaRepository<Reply,Long> {
}

3.연관관계 테스트

3-1. 테스트 데이터 추가하기

  • Member 객체 100개 추가
import com.example.board2022.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 // Member 객체 100개  생성
    public void insertMembers(){

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

            Member member = Member.builder()
                    .email("user" + i + "@aaa.com")
                    .password("1111")
                    .name("USER" + i)
                    .build();

            memberRepository.save(member);
        });
    }
}
  • Member 데이터 이용해 Board 객체 생성하여 추가

import com.example.board2022.entity.Board;
import com.example.board2022.entity.Member;
import com.example.board2022.entity.Reply;
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 BoardRepositoryTests {

    @Autowired
    BoardRepository boardRepository;

    @Test // 게시글 100개 생성 -> 한 명의 사용자가 하나의 게시물 등록하도록
    public void insertBoard(){

        IntStream.rangeClosed(1,100).forEach(i -> {
            Member member = Member.builder().email("user" + i + "@aaa.com").build();

            Board board = Board.builder()
                    .title("Title..." + i)
                    .content("Content..." + i)
                    .writer(member)
                    .build();

            boardRepository.save(board);
        });
    } 

}
  • 특정한 임의의 게시글을 대상으로 댓글 추가
import com.example.board2022.entity.Board;
import com.example.board2022.entity.Reply;
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 ReplyRepositoryTests {

    @Autowired
    ReplyRepository replyRepository;

    @Autowired
    BoardRepository boardRepository;

    @Test // 임의의 게시글을 대상으로 댓글추가(300개)
    public void insertReply(){

        IntStream.rangeClosed(1,300).forEach(i -> {
            // 1부터 100까지 임의의 번호
            long bno = (long)(Math.random() * 100) + 1;

            Board board = Board.builder().bno(bno).build();

            Reply reply = Reply.builder()
                    .text("Reply...." + i)
                    .board(board)
                    .replyer("guest")
                    .build();

            replyRepository.save(reply);

        });
    }

}

4.@ManyToOne 과 Eager/Lazy loading

  • Entity 클래스들이 실제 DB상에서는 두개 혹은 그이상의 테이블로 생성되기 때문에 연관관계를 맺고 있다는 것은 DB 입장에서 보면 JOIN이 필요하다
  • @ManyToOne의 경우 FK 쪽의 엔티티를 가져올 때 PK 쪽의 엔티티도 같이 가져온다

4-1.@ManyToOne 테스트

Member를 @ManyToOne 으로 참조하고 있는 Board 를 조회


import com.example.board2022.entity.Board;
import com.example.board2022.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.Optional;
import java.util.stream.IntStream;

@SpringBootTest
public class BoardRepositoryTests {

    @Autowired
    BoardRepository boardRepository;

    @Test
    public void testRead1(){
        Optional<Board> result = boardRepository.findById(100L); // DB에 존재하는 번호

        Board board = result.get();

        System.out.println(board);
        System.out.println(board.getWriter());
    }
}

실행된 쿼리

  • left outer join 처리 된것을 확인할 수 있다
Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.moddate as moddate2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.content as content4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer_email as writer_e6_0_0_,
        member1_.email as email1_1_1_,
        member1_.moddate as moddate2_1_1_,
        member1_.regdate as regdate3_1_1_,
        member1_.name as name4_1_1_,
        member1_.password as password5_1_1_ 
    from
        board board0_ 
    left outer join
        member member1_ 
            on board0_.writer_email=member1_.email 
    where
        board0_.bno=?
Board(bno=100, title=Title...100, content=Content...100, writer=Member(email=user100@aaa.com, password=1111, name=USER100))
Member(email=user100@aaa.com, password=1111, name=USER100)

위와 같이 여러 테이블이 조인으로 처리되는 상황은 효율적이지 않다 => Lazy loading을 권장한다

5.Eager loading

특정 엔티티를 조회할 때 연관관계를 가진 모든 엔티티를 같이 로딩하는 것을 "Eager loading(즉시로딩)"이라고 한다

  • 즉시로딩의 장점 : 한 번에 연관관계가 있는 모든 엔티티를 가져온다
  • 즉시로딩의 단점 : 여러 연관관계를 맺고 있거라 연관관계가 복잡할수록 조인으로 인한 성능 저하가 나타난다
    ☆ JPA에서 연관관계의 데이터를 어떻게 가져올 것인가를 fetch(패치)라고 한다
    => 연관관계의 어노테이션의 속성으로 fetch 모드를 지정한다 , 즉시로딩에 반대되는 개념인 Lazy loading 으로 처리하는 것이 좋다
  • Lazy loading : 지연로딩

5-1.Lazy loading 적용


import lombok.*;
import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "writer") // exclude : toString 대상에서 제외한다
public class Board extends BaseEntity{

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

    private String title;

    private String content;

    // LAZY : 필요할 때만 사용, LAZY 사용하면 @ToString(exclude) 무조건 사용!
    @ManyToOne(fetch = FetchType.LAZY)
    private Member writer;

}

5-2.Lazy loading 테스트

아래 테스트 코드를 보면 지연로딩 방식으로 로딩하기 때문에 Board 테이블만 가져오는것은 문제가 없지만 board.getWriter() 메소드에서 문제가 발생한다 => board.getWriter() 는 member 테이블을 로딩해야 하는데 이미 DB 연결이 끝난 상태 이므로 문제가 발생된다

  • 해결방법 : @Transactional 어노테이션을 추가한다 => 해당 메서드를 하나의 트랜잭션 으로 처리하라는 의미로 필요할 때 다시 DB 와 연결된다
import com.example.board2022.entity.Board;
import com.example.board2022.entity.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.transaction.Transactional;
import java.util.Optional;
import java.util.stream.IntStream;

@SpringBootTest
public class BoardRepositoryTests {

    @Autowired
    BoardRepository boardRepository;

    @Test
    @Transactional // 필요한 순간 다시 DB 연결
    public void testRead1(){
        Optional<Board> result = boardRepository.findById(100L); // DB에 존재하는 번호

        Board board = result.get();

        System.out.println(board);
        System.out.println(board.getWriter());
    }

}
  • 실행된 쿼리

    처음에는 board 테이블만 로딩하여 처리한 후 getWriter()를 처리하기 위해 member 테이블을 로딩한다

Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.moddate as moddate2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.content as content4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer_email as writer_e6_0_0_ 
    from
        board board0_ 
    where
        board0_.bno=?
Board(bno=100, title=Title...100, content=Content...100)
Hibernate: 
    select
        member0_.email as email1_1_0_,
        member0_.moddate as moddate2_1_0_,
        member0_.regdate as regdate3_1_0_,
        member0_.name as name4_1_0_,
        member0_.password as password5_1_0_ 
    from
        member member0_ 
    where
        member0_.email=?
Member(email=user100@aaa.com, password=1111, name=USER100)

6.@ToString() 사용시 주의점

@ToString()은 해딩 클래스의 모든 멤버 변수를 출력하게 된다
☆ ex) Board 객체의 @ToString()을 하면 writer 변수로 선언된 Member 객체도 함께 출력해야 하며 Member 객체를 출력하기 위해서는 Member 객체의 @ToString()이 호출되어야 하고 이때 DB 연결이 필요하게 된다

  • 위와같은 문제로 연관관계가 있는 엔티티 클래스의 경우 @ToString()을 사용 할때에는 exclude 속성을 사용하는 것이 좋다
    => exclude : 해당 속성값으로 지정된 변수는 toString()에서 제외 한다 -> 지연로딩 지정시 반드시 지정해 주는 것이 좋다

7.Lazy loading(지연로딩)의 장/단점

  • 장점 : 조인을 하지 않기 때문에 하나의 테이블을 이용하는 경우 빠른 속도로 처리가 가능하다
  • 단점 : 필요한 순간에 쿼리를 실행해야 하기 때문에 연관관계가 복잡한 경우 여러번의 쿼리가 실행된다
    => 따라서 지연로딩을 기본으로 사용하고, 상황에 맞게 필요한 방법을 찾는 것이 중요하다
profile
비전공초보개발자

0개의 댓글