Eager Loading & Lazy Loading
아직 JPA랑 좀 서먹서먹한 관계라 이 친구에 대해 잘 알지 못 한다.
Eager Loading 과 Lazy Loading에 대해 처음 들었을 때는 '그게 뭔데;;' 라는 생각이 들었고,
누군가가 설명해줬을 때는 '그런게 된다고?' 라는 생각이 들었다.
JPA랑 조금 더 친해져 보자.
Eager
일반적인 게시판을 생각했을 때, 게시글(Post)와 사용자(Member)의 관계는 N:1 이다.
// Member Entity
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
...
}
// Post Entity
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Member member;
...
}
위와 같이 Entity를 구성하고, 각각 Repository를 작성한 뒤,postRepository.findById()
메서드를 통해 실행될 쿼리를 예상해 보면 다음과 같다.
select * from post
left join member
on member.id = post.member_id
where post.id = ?
예상대로 쿼리가 실행되는지 확인해보자.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PostRepositoryTest {
@Autowired
private PostRepository posts;
@Autowired
private MemberRepository members;
@Test
void findById() {
//given
final Member member = members.save(new Member(null, "polo"));
final Post post = posts.save(new Post(null, "제목1", member));
//when
final Post result = posts.findById(post.getId()).get();
//then
assertThat(result.getTitle()).isEqualTo("제목1");
}
}
@ManyToOne
어노테이션에 따로 fetch type을 지정 안 했으니, default 인 Eager로 실행 될 것이다.
예상되로라면 join
쿼리가 포함된 select
쿼리가 실행될 것이다.
Hibernate:
insert
into
member
(name)
values
(?)
Hibernate:
insert
into
post
(member_id,title)
values
(?,?)
그런데 예상과 달리 insert
쿼리만 실행된다.
아 영속성 컨텍스트에 캐시된 데이터를 그대로 가져와서 사용하기 때문에 select
쿼리가 실행되지 않았나 보다.
어거지로 영속성 컨텍스트에 있는 내용을 영속화 하고 비워주자.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PostRepositoryTest {
@Autowired
private PostRepository posts;
@Autowired
private MemberRepository members;
@Autowired
private TestEntityManager em;
@Test
@DisplayName("모든 post를 가져온다.")
void findAll() {
//given
final Member member = members.save(new Member(null, "polo"));
final Post post = posts.save(new Post(null, "제목1", member));
em.flush();
em.clear();
//when
final Post result = posts.findById(post.getId()).get();
//then
assertThat(result.getTitle()).isEqualTo("제목1");
}
}
위와 같이 flush()
와 clear()
로 영속화 후, 컨텍스트를 비워주니 예상한대로 join
을 포함한 select
쿼리가 실행된다.
Hibernate:
insert
into
member
(name)
values
(?)
Hibernate:
insert
into
post
(member_id,title)
values
(?,?)
Hibernate:
select
p1_0.id,
m1_0.id,
m1_0.name,
p1_0.title
from
post p1_0
left join
member m1_0
on m1_0.id=p1_0.member_id
where
p1_0.id=?
Lazy
이번엔 Lazy 로딩을 이용해 보자.
Lazy 로딩은 위의 Eager방식 처럼 객체를 조회 할때 조회 된 객체가 참조하고 있는 다른 객체들을 함께 로드 하는것이 아니라,
처음에는 필요한 주객체만 로딩하고, 나중에 실제로 참조객체가 필요한 시점에 그때서야 로딩한다.
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY) // EAGER(default) -> LAZY
private Member member;
...
}
Post Entity
에 fetchType을 LAZY로 변경을 해주었다.
이렇게 하고 아까의 테스트코드를 다시 실행시키면,
// insert 생략
Hibernate:
select
p1_0.id,
p1_0.member_id,
p1_0.title
from
post p1_0
where
p1_0.id=?
Hibernate:
select
m1_0.id,
m1_0.name
from
member m1_0
where
m1_0.id=?
이렇게 나온다.
이게 진짜 되는거 맞음?
흐음. join이 아닌 select쿼리가 두개 실행되는 것은 확인이 되는데,
이게 정말 post가 참조하고 있는 member의 값을 사용하려고 할때 쿼리가 실행되는게 맞는걸까?
의심병 말기 환자다보니 확인을 해봐야겠다.
테스트 코드를 좀 변경했다.
@Test
@DisplayName("모든 post를 가져온다.")
void findAll() throws InterruptedException {
final Member member = members.save(new Member(null, "polo"));
final Post post = posts.save(new Post(null, "제목1", member));
em.flush();
em.clear();
System.out.println();
System.out.println(LocalDateTime.now());
System.out.println("post 조회");
final Post result = posts.findById(post.getId()).get();
System.out.println("title : " + result.getTitle());
System.out.println();
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
System.out.println();
System.out.println(LocalDateTime.now());
System.out.println("member 조회");
System.out.println("memberName : " + result.getMember().getName());
System.out.println();
};
int delay = 3;
executor.schedule(task, delay, TimeUnit.SECONDS);
Thread.sleep(4000);
executor.shutdown();
}
처음에는 JPA를 통해 post의 데이터를 가져온 뒤, post의 제목을 출력하고,
3초의 딜레이를 준 뒤, post가 참조하고 있는 member의 이름을 출력하게 해보았다.
2023-07-16T17:08:29.317118
post 조회
Hibernate:
select
p1_0.id,
p1_0.member_id,
p1_0.title
from
post p1_0
where
p1_0.id=?
title : 제목1
2023-07-16T17:08:32.341331
member 조회
Hibernate:
select
m1_0.id,
m1_0.name
from
member m1_0
where
m1_0.id=?
memberName : polo
호오.. JPA를 통해 post 데이터를 가져온 시간은 17시 8분 29초이고,
조회된 데이터가 참고하고 있는 member를 사용한 시점은 17시 8분 32초 이다.
그리고 이 시점에 쿼리가 실행되고 있는 것을 확인할 수 있었다.
이제 믿을 수 있다.
Lazy 로딩 신기하다.
Lazy 로딩이 유용한건 사실이나 N + 1 문제 이외에도 다른 문제가 발생할 수 있는 요지가 남아있다는것 역시 사실이다.
이러한 문제점들을 해결하기 위해 여러 방식들이 사용된다고 하는데...
아직 JPA랑 그정도 사이는 아닌 듯 하다.
다음에 알아보자.