본문 바로가기
Study/Java

Eager Loading & Lazy Loading

by dev_kong 2023. 7. 16.
728x90
728x90

아직 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랑 그정도 사이는 아닌 듯 하다.


다음에 알아보자.

728x90
728x90

댓글