📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/board2022
JPA에서 관계를 고민할 때는 FK쪽을 먼저 해석하면 편리하다 앞에서 만든 Board 와 Member의 관계는 N:1(다대일) 관계가 되므로
@ManyToOne 어노테이션을 적용해야 한다
- @ManyToOne : DB상에서 외래키의 관계로 연결된 엔티티 클래스에 설정한다
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;
}
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; // 연관관계 지정
}
import com.example.board2022.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<Board,Long> {
}
import com.example.board2022.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member,String> {
}
import com.example.board2022.entity.Reply;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReplyRepository extends JpaRepository<Reply,Long> {
}
- 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);
});
}
}
- Entity 클래스들이 실제 DB상에서는 두개 혹은 그이상의 테이블로 생성되기 때문에 연관관계를 맺고 있다는 것은 DB 입장에서 보면 JOIN이 필요하다
- @ManyToOne의 경우 FK 쪽의 엔티티를 가져올 때 PK 쪽의 엔티티도 같이 가져온다
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을 권장한다
특정 엔티티를 조회할 때 연관관계를 가진 모든 엔티티를 같이 로딩하는 것을 "Eager loading(즉시로딩)"이라고 한다
- 즉시로딩의 장점 : 한 번에 연관관계가 있는 모든 엔티티를 가져온다
- 즉시로딩의 단점 : 여러 연관관계를 맺고 있거라 연관관계가 복잡할수록 조인으로 인한 성능 저하가 나타난다
☆ JPA에서 연관관계의 데이터를 어떻게 가져올 것인가를 fetch(패치)라고 한다
=> 연관관계의 어노테이션의 속성으로 fetch 모드를 지정한다 , 즉시로딩에 반대되는 개념인 Lazy loading 으로 처리하는 것이 좋다- 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;
}
아래 테스트 코드를 보면 지연로딩 방식으로 로딩하기 때문에 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)
@ToString()은 해딩 클래스의 모든 멤버 변수를 출력하게 된다
☆ ex) Board 객체의 @ToString()을 하면 writer 변수로 선언된 Member 객체도 함께 출력해야 하며 Member 객체를 출력하기 위해서는 Member 객체의 @ToString()이 호출되어야 하고 이때 DB 연결이 필요하게 된다
- 위와같은 문제로 연관관계가 있는 엔티티 클래스의 경우 @ToString()을 사용 할때에는 exclude 속성을 사용하는 것이 좋다
=> exclude : 해당 속성값으로 지정된 변수는 toString()에서 제외 한다 -> 지연로딩 지정시 반드시 지정해 주는 것이 좋다
- 장점 : 조인을 하지 않기 때문에 하나의 테이블을 이용하는 경우 빠른 속도로 처리가 가능하다
- 단점 : 필요한 순간에 쿼리를 실행해야 하기 때문에 연관관계가 복잡한 경우 여러번의 쿼리가 실행된다
=> 따라서 지연로딩을 기본으로 사용하고, 상황에 맞게 필요한 방법을 찾는 것이 중요하다