ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코드로 배우는 스프링부트 웹 프로젝트 Day 13
    Spring/코드로 배우는 스프링부트 웹 프로젝트 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)의 장/단점 ]

     

    장점

        • 조인을 하지 않기 때문에 단순하게 하나의 테이블을 이용할 경우에는 빠른 속도의 처리가 가능함

     

    단점

        • 하나의 테이블만 사용하는 것이 아니라면, 필요한 순간에 쿼리를 실행해야 하기 때문에 연관관계가 복잡한 경우에는 여러 번의 쿼리가 실행됨

     

    📍 지연 로딩을 기본으로 사용하고, 상황에 맞게 필요한 방법을 찾기

     

     

Designed by Tistory.