지연 로딩 과 즉시 로딩

랏 뜨·2024년 12월 10일

🔎 Overview

  스프링으로 개발을 하던 중, Entity와 Repository를 생성하고 특정 필드의 값을 가져오려고 하니 에러가 발생했다.
그 이유는 @OneToMany 어노테이션을 설정해둔 List 타입의 필드 때문이었다.

  분명히 일반 @Column 이나 @ManyToOne 으로 설정해둔 필드들은 이상 없이 값들을 가져오는데, @OneToMany로 설정해둔 값들은 왜 가져오지 못하는걸까?
단순히 1:N 으로 여러 개의 엔티티와 연결되어 있기 때문일까?

  이전부터 궁금했던 사항이기에 더 깊게 공부해보고자 했다.


1️⃣ 생성한 엔티티

@Getter
@Setter
@Entity
public class Question {
	// 생략
    
    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<Answer> answerList;
}

// Question과 동일
public class Answer {
	// 생략
    
    @ManyToOne
    private Question question;
}
  • 질문 글에 대한 엔티티 : Question
    • @OneToMany : 한 질문에는 여러 답변이 달릴 수 있음
    • cascade = CascadeType.REMOVE : 질문이 삭제되면 그에 딸린 값(해당 엔티티를 외래키로 가진)도 함께 삭제
      • 1번 Question에 Answer가 2개 있다면, 1번 Question을 삭제할 시 Answer 2개를 모두 삭제
  • 답변 대한 엔티티 : Answer
    • @ManyToOne : 여러 개의 답글이 한 질문에 달릴 수 있음

2️⃣ 에러가 발생한 코드

@Test
void getAnswerListTest() {
	Question q = questionRepository.findById(2)
    				.orElseThrow(RunTimeException::new);
                    
	List<Answer> answerList = q.getAnswerList();
    
    assertEquals(1, answerList.size());
    assertEquals("내용", answerList.get(0).getContent());
}
  • 테스트코드 작성 후 Junit을 통한 테스트 실행

‼️ 에러 발생


3️⃣ 에러 발생 흐름

1) QuestionRepository 에서 findById 메서드 호출

  • DB와 연결 후, findById 메서드를 사용해 ID가 2인 데이터를 가져옴
  • 하지만 이 때, answerList 는 가져오지 않음 (이유는 후에 서술)

2) DB와의 연결(세션)이 끊어짐

  • findById 를 사용한 후 DB와의 연결이 종료됨

3) q.getAnswerList() 호출

  • Question 데이터의 필드인 answerList 를 가져오려고 시도
  • DB연결이 이미 끊어졌기 때문에, 데이터를 가져오지 못하고 오류 발생

❗ 이러한 에러가 발생한 이유는 지연 로딩이 발생했기 때문!!!


📕 지연로딩과 즉시 로딩

데이터를 가져오는 두 가지의 방법

1) 지연 로딩 (Lazy Loading)

  • 데이터를 미리미리 받아오지 않고 필요할 때마다 DB에서 꺼내쓰는 방식
  • @ManyToMany , @OneToMany 의 옵션인 fetch 의 기본 타입
  • 속도가 빠르고 가벼움
  • 나중에 데이터를 추가로 가져오려면 DB와의 연결이 필수
  • DB와의 연결이 종료되면, DB에서 데이터를 받아올 수 없음

2) 즉시 로딩 (Eager Loading)

  • 처음부터 존재하는 관련된 모든 데이터를 DB에서 가져오는 방식
  • @OneToOne, @ManyToOne 의 옵션인 fetch 의 기본 타입
  • 처음 로딩 시에 모든 데이터를 가져오기 때문에 상대적으로 느리고 무거움
  • - 위에서는 List` 를 받아올 때 문제가 발생
  • Answer 엔티티의 필드 중 Question 필드는 @ManyToOne 이기 때문에
  • 이후에 DB와의 연결이 끊어지더라도, 메모리 상에 데이터가 존재하기 때문에 문제가 없음

🤔 지연 로딩에서는 왜 List 타입을 가져오지 않았을까?

JPA는 일반 필드와 연관 데이터를 다르게 처리하기 때문

  • 일반 필드
    • ex) Integer, String ...
    • findById 로 Question 을 가져올 때, 일반 필드의 데이터들은 미리 가져옴
    • 메모리에 이미 적재되었기 때문에, DB와의 세션이 끊어진 후에도 사용할 수 있음
  • 연관 데이터
    • 다른 엔티티와 연결된 필드
    • answerList 는 Answer 엔티티와 연관된 데이터
    • JPA 는 설정에 따라 이러한 데이터를 바로바로 가져오지 않음
    • 앞서 말했듯 @OneToMany 나 @ManyToMany 의 기본 FetchType은 지연 로딩

📝 List가 지연로딩인 이유

1) 효율성

  • List 는 여러 개의 값을 포함할 수 있음
  • DB에서 한 번에 모든 값들을 무조건 다 가져오면 시간 및 공간적인 측면에서 성능이 떨어질 수 있음
  • 그러므로 많은 데이터를 가져오는 필드는 더 좋은 효율성을 위해 기본값을 지연 로딩으로 설정

2) 필요성

  • 모든 데이터를 연관 데이터를 다 사용해야만 하지는 않음
  • title 값만 필요한데, 굳이 answerList의 값을 가져올 필요가 없음
  • 지연로딩을 통해 이러한 불필요한 데이터 적재를 최소화

↔️ 일반 필드와 List가 다른 이유

  • 일반 필드의 경우, 한 번의 테이블 연결로 DB에서 바로 읽어올 수 있음
    • ex) SELECT id FROM question
  • 여기서 사용한 answerList 의 경우 다른 테이블 answer 과 연관되어 있음
    • 이 경우, Question 을 가져와도 List 의 Answer 데이터를 가져오려면 또 다른 쿼리를 실행해야 함

⭕ 기존 코드 수정

1) 관련된 엔티티 필드의 로딩 전략을 즉시 로딩으로 수정

@Getter
@Setter
@Entity
public class Question {
	// 생략
    
    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
    private List<Answer> answerList;
}

// Question과 동일
public class Answer {
	// 생략
    
    @ManyToOne(fetch = FetchType.EAGER)
    private Question question;
}
  • 성능적인 측면으로 인해 선호되는 방법은 아님

2) 사용되는 메서드에 @Transactional 어노테이션 사용

@Transactional
@Test
void getAnswerListTest() {
	Question q = questionRepository.findById(2)
    				.orElseThrow(RunTimeException::new);
                    
	List<Answer> answerList = q.getAnswerList();
    
    assertEquals(1, answerList.size());
    assertEquals("내용", answerList.get(0).getContent());
}
  • 작업이 끝날 때까지 세션의 연결을 끊지 않도록 Transaction 으로 만들어줌
  • 즉시로딩 방법을 사용하더라도, DB와의 연결이 종료되지 않았으므로 얼마든지 데이터를 다시 가져올 수 있음

  • 사실 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에, 위와 같은 처리가 필요 없을 수 있음

📑 지연로딩과 즉시로딩의 차이점 정리

특성Lazy LoadingEager Loading
데이터 로드 시점필요한 순간 로드처음부터 모든 데이터를 로드
성능초기 로드가 가볍지만 필요 시 추가 쿼리초기 로드가 무거울 수 있음
사용 사례데이터가 많거나 자주 사용하지 않는 경우연관 데이터를 항상 함께 사용하는 경우

참고) OpenAI. (2024).ChatGPT(4o)[Large language model].https://chatgpt.com/

profile
기록

0개의 댓글