Spring/코드로 배우는 스프링부트 웹 프로젝트

코드로 배우는 스프링부트 웹 프로젝트 Day 13

한 면만 쓴 종이 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)의 장/단점 ]

 

장점

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

 

단점

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

 

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