-
코드로 배우는 스프링부트 웹 프로젝트 Day 13Spring/코드로 배우는 스프링부트 웹 프로젝트 2022. 9. 12. 19:57
/* Annotation들은 스프링부트 프로젝트의 Annotation 정리 페이지에 따로 정리해두었습니다. */
[ 연관관계 테스트 ]
현재 3개의 테이블이 PK와 FK의 관계로 이루어져 있기 때문에 테스트를 위한 데이터를 추가하는 작업도 PK 쪽에서부터 시작하는 것이 좋음
test 클래스들 생성
[ 테스트 데이터 추가하기 ]
MemberRespositoryTests 에 MemberRepository를 주입
예제로 사용할 Member객체를 100개 주입
MemberRepositoryTests 클래스
package org.zerock.board2.repository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.zerock.board2.entity.Member; 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("user" + i + "@aaa.com") .password("1111") .name("User" + i) .build(); memberRepository.save(member); }); } }
위 테스트의 결과로 아래와 같이 데이터가 추가되었다.
moddate와 regdate가 NULL로 저장되어있다.
Application 클래스에 @EnableJpaAuditing 을 추가하지 않았기 때문이다.
자동으로 시간을 처리하도록 하기 위해 @EnableJpaAuditing를 추가한다.
@SpringBootApplication @EnableJpaAuditing public class Board2Application { public static void main(String[] args) { SpringApplication.run(Board2Application.class, args); } }
BoardRepositoryTests 클래스
package org.zerock.board2.repository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.zerock.board2.entity.Board; import org.zerock.board2.entity.Member; import java.util.stream.IntStream; @SpringBootTest public class BoardRepositoryTests { @Autowired private BoardRepository boardRepository; @Test 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); }); } }
ReplyRepositoryTests 클래스
package org.zerock.board2.repository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.zerock.board2.entity.Board; import org.zerock.board2.entity.Reply; import java.util.stream.IntStream; @SpringBootTest public class ReplyRepositoryTests { @Autowired private ReplyRepository replyRepository; @Test 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); }); } }
300개의 댓글을 1~100 사이의 번호로 추가
[ 필요한 쿼리 기능 정리하기 ]
현재 화면에 필요한 데이터
- 목록화면: 게시글의 번호, 제목, 댓글 개수, 작성자의 이름/이메일
- 조회 화면: 게시글의 번호, 제목, 내용, 댓글 개수, 작성자 이름/이메일
[ @ManyToOne과 Eager/Lazy loading ]
두 개 이상의 엔티티 간의 연관관계를 맺으면 엔티티 클래스들이 실제 데이터베이스상에서는 두 개 혹은 두 개 이상의 테이블로 생성됨 => 연관관계를 맺고 있다는 것은 데이터베이스의 입장으로 보면 조인이 필요함
@ManyToOne의 경우, FK 쪽의 엔티티를 가져올 때 PK 쪽의 엔티티도 같이 가져옴
Member를 @ManyToOne으로 참조하고 있는 Board를 조회하는 테스트 코드
BoardRepositoryTests 클래스 일부
@Test public void testRead1() { Optional<Board> result = boardRepository.findById(100L); // 데이터베이스에 존재하는 번호 Board board = result.get(); System.out.println(board); System.out.println(board.getWriter()); }
쿼리가 내부적으로 left outer join 처리 된 것을 확인할 수 있음
ReplyRepositoryTests 클래스
@Test public void readReply1() { Optional<Reply> result = replyRepository.findById(1L); Reply reply = result.get(); System.out.println(reply); System.out.println(reply.getBoard()); }
위의 실행된 SQL을 보면, reply, board, member 테이블까지 모두 조인으로 처리된 것을 확인 가능
Reply를 가져올 때 매번 Board와 Member까지 조인해서 가져올 필요가 많지는 않으므로, 그다지 효율적인 편은 아님
[ fetch는 Lazy loading을 권장]
Eager loading (즉시 로딩)
• 특정한 엔티티를 조회할 때 연관관계를 가진 모든 엔티티를 같이 로딩하는 것
• 장점: 한 번에 연관관계가 있는 모든 엔티티를 가져옴
• 단점: 연관관계를 많이 맺거나 복잡할수록 조인으로 인한 성능 저하를 피할 수 없음
fetch
• JPA에서 연관관계의 데이터를 어떻게 가져올 것인가
• 연관관계의 어노테이션의 속성으로 'fetch' 모드 지정
Lazy loading (지연 로딩)
• Eager loading과 반대되는 개념
Board 클래스 수정
public class Board extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long bno; private String title; private String content; @ManyToOne(fetch = FetchType.LAZY) // 명시적으로 Lazy 로딩 지정 private Member writer; // 연관관계 지정 }
ManyToOne 어노테이션에 fetch 속성을 명시하고 FetchType.LAZY 지연로딩을 적용
이제 BoardRepositoryTests의 testRead1()을 실행하면 아래와 같은 예외가 발생함
위 메시지는 데이터베이스와 추가적인 연결이 필요하다는 뜻
@Test public void testRead1() { Optional<Board> result = boardRepository.findById(100L); // 데이터베이스에 존재하는 번호 Board board = result.get(); System.out.println(board); System.out.println(board.getWriter()); }
위 코드에서 board 테이블만 가져와서 System.out.println()을 하는 것은 문제가 없지만 board.getWriter()에서 문제가 발생하게 됨
board.getWriter()은 member 테이블을 로딩해야 하는데 이미 데이터베이스와의 연결은 끝난 상태이기 때문
=> noSession 오류 발생
이를 해결하려면 데이터베이스와 다시 연결해야 함 => @Transactional을 메서드의 선언부에 추가
@Transactional @Test public void testRead1() { Optional<Board> result = boardRepository.findById(100L); // 데이터베이스에 존재하는 번호 Board board = result.get(); System.out.println(board); System.out.println(board.getWriter()); }
처음에는 board 테이블만 로딩해서 처리하고 있지만, 후에 board.getWriter()를 하기 위해서 member 테이블을 로딩함
지연로딩을 사용하지 않았을 때, 자동으로 board 테이블과 member 테이블이 조인으로 처리되는 것과 차이가 있음을 확인 가능
[ 연관관계에서는 @ToString()을 주의 ]
엔티티 간에 연관관계를 지정하는 경우에는 항상 @ToString() 을 주의해야 함
@ToString()은 해당 클래스의 모든 멤버 변수를 출력하게 됨
즉, Board 객체의 @ToString() 을 하면 writer 변수로 선언된 Member 객체도 출력해야 함 => Member 변수의 toString()이 호출되어야 함 => 데이터베이스 연결 필요
👉 연관관계가 있는 엔티티 클래스의 경우 @ToString() 을 할 때는 exclude 속성 사용하는 것이 좋음
exclude의 속성값으로 지정된 변수는 toString()에서 제외하기 때문에 지연 로딩을 할 때는 반드시 지정해주기
[ 지연 로딩의(lazy loading)의 장/단점 ]
장점
• 조인을 하지 않기 때문에 단순하게 하나의 테이블을 이용할 경우에는 빠른 속도의 처리가 가능함
단점
• 하나의 테이블만 사용하는 것이 아니라면, 필요한 순간에 쿼리를 실행해야 하기 때문에 연관관계가 복잡한 경우에는 여러 번의 쿼리가 실행됨
📍 지연 로딩을 기본으로 사용하고, 상황에 맞게 필요한 방법을 찾기
'Spring > 코드로 배우는 스프링부트 웹 프로젝트' 카테고리의 다른 글
코드로 배우는 스프링부트 웹 프로젝트 Day 15 (0) 2022.09.19 코드로 배우는 스프링부트 웹 프로젝트 Day 14 (0) 2022.09.14 코드로 배우는 스프링부트 웹 프로젝트 Day 12 (0) 2022.09.07 코드로 배우는 스프링부트 웹 프로젝트 Day 11 (0) 2022.08.26 코드로 배우는 스프링부트 웹 프로젝트 Day 10 (0) 2022.08.25