본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
JPA

프록시 객체와 지연 로딩, 즉시 로딩 - 2

by 방구석 대학생 2020. 10. 16.

"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."

www.inflearn.com/course/ORM-JPA-Basic#

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그

www.inflearn.com

 

즉시 로딩에 있어서 주의 할 점?

* 실무에서는 즉시 로딩은 사실상 사용하면 안된다. 가급적 지연 로딩만 사용하자.

 

그렇다면 어째서 즉시 로딩은 사용하면 안되는 걸까?

 

* 즉시 로딩을 적용하면 전혀 예상하지 못한 SQL 이 발생하게 된다.

 

- 우선 데이터베이스 입장에서 테이블이 1,2 개만 그닥 느리지 않다.

하지만 실무에서는 보통 테이블의 갯수가 수십개가 넘어가는데, 그렇게 되면 완전히 다른 차원의 문제가 되어 버린다.

그럴 경우 find 메소드를 호출하게 되면 EAGER 로 되어있는 테이블들이 전부 다 조인하게 된다.

실무에서 테이블이 복잡하게 얽혀 있는 상황이라면 그냥 다 지연 로딩(LAZY) 을 통해 연관관계에 있는 도메인 클래스의 객체들을 프록시 객체로 생성해주자.

 

 

* 즉시 로딩은 JPQL 에서 N + 1 문제를 일으킨다.

find 메소드의 경우 기본 키를 찍어서 가져오는 것이기 때문에 JPA 가 내부적으로 최적화를 할 수 있다.

그런데 JPQL 은

List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList(); 과 같은 코드에서 select m from Member m 과 같은 쿼리가 그대로 SQL 로 우선적으로 번역된다.

(일단 select * from Member 쿼리가 나간다.)

 

그러면 당연히 Member 에만 select 쿼리가 나간다.

그렇게 Member 데이터를 가져왔더니 Team 클래스의 객체가 즉시 로딩(가져올 때 무조건 값이 다 들어가 있어야 한다.)으로 선언되어 있으면, Member 쿼리가 나간 후 호출되는 Member 의 갯수만큼 EAGER(즉시 로딩) 으로 Team 클래스의 데이터를 가져오기 위해 별도로 쿼리가 나가게 된다. 

 

- JpaMain.java

List<Member> members = em.createQuery("select m from Member m", Member.class)
					.getResultList();

- hibernate SQL

Hibernate: // Member select 한 번 Team select 한 번, select 쿼리가 총 두 번 나오는 것을 볼 수 있다. 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.createdBy as createdb2_3_,
            member0_.createdDate as createdd3_3_,
            member0_.lastModifiedBy as lastmodi4_3_,
            member0_.lastModifiedDate as lastmodi5_3_,
            member0_.team_TEAM_ID as team_tea7_3_,
            member0_.USERNAME as username6_3_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.createdBy as createdb2_7_0_,
        team0_.createdDate as createdd3_7_0_,
        team0_.lastModifiedBy as lastmodi4_7_0_,
        team0_.lastModifiedDate as lastmodi5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

 

이번엔 아래와 같이 코드를 작성한 후 애플리케이션을 실행해보자.

// Team 데이터 2개 생성
Team team = new Team();
team.setName("teamA");
em.persist(team);

Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);

// 2개의 Member 데이터에 각자 다른 Team 데이터를 부여한다.
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamB);
em.persist(member2);

em.flush();
em.clear();

// JPQL 문 실행
List<Member> members = em.createQuery("select m from Member m", Member.class)
                    .getResultList();

 

- hibernate SQL

Hibernate: // 일단 Member 데이터를 한번이 다 들고 왔다.
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.createdBy as createdb2_3_,
            member0_.createdDate as createdd3_3_,
            member0_.lastModifiedBy as lastmodi4_3_,
            member0_.lastModifiedDate as lastmodi5_3_,
            member0_.team_TEAM_ID as team_tea7_3_,
            member0_.USERNAME as username6_3_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.createdBy as createdb2_7_0_,
        team0_.createdDate as createdd3_7_0_,
        team0_.lastModifiedBy as lastmodi4_7_0_,
        team0_.lastModifiedDate as lastmodi5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.createdBy as createdb2_7_0_,
        team0_.createdDate as createdd3_7_0_,
        team0_.lastModifiedBy as lastmodi4_7_0_,
        team0_.lastModifiedDate as lastmodi5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

 

위의 SQL 문을 보면 Member 테이블을 select 한 이후에 보니 Member 와 연관관계를 가지고 있는 Team 클래스의 데이터가 2개 존재하여 select 쿼리를 2번 날려서 해당 데이터를 가져온 것을 볼 수 있다.(다른 팀이기 때문에 영속성 컨텍스트에도 없어서 따로따로 가져와야 한다.)

 

더 많은 숫자의 데이터가 이런 방식으로 구성되어 있다면 쿼리가 속칭 N + 1(N 이 결과가 10개면 최초 쿼리 1개로 인해 하나가 더 추가되어 11개의 쿼리가 나오게 된다.) 문제를 일으키게 된다.(즉시 로딩의 문제점)

 

그런데 이를 LAZY 방식으로 바꾸면 JPQL 에서 쿼리를 보낸 Member 클래스에 대한 데이터를 제외한 다른 데이터, 즉 연관관계를 가지고 있는 Team 클래스에 대한 데이터에 대해 select 와 같은 쿼리가 나오지 않게 된다.

(아직 사용하고 있지 않기 때문 - 프록시 객체로 Team 클래스의 객체가 생성된다.)

 

위의 설명과 같이 모든 연관관계를 LAZY 방식으로 연관관계의 프록시 객체로 생성해준 다음, N + 1 과 같은 문제를 해결하면서 즉시 로딩을 수행 할 수 있는 방법이 있다.

 

* JPQL Fetch Join

: 동적으로 원하는 데이터를 선택하여 한번에 가져오는 것(LAZY 설정으로 인해 프록시 객체로 생성된 객체에 대해 즉시 로딩으로 한방에 테이블을 조인해서 데이터를 같이 가져올 수 있다.)

-> 다음에 JPQL 관련 강의에서 자세히 설명된다.

- JpaMain.java

// JPQL Fetch Join(LAZY 설정에서 즉시 로딩)
 List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class)
                    .getResultList();

 - hibernate SQL

Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.MEMBER_ID as member_i1_4_0_,
            team1_.TEAM_ID as team_id1_9_1_,
            member0_.createdBy as createdb2_4_0_,
            member0_.createdDate as createdd3_4_0_,
            member0_.lastModifiedBy as lastmodi4_4_0_,
            member0_.lastModifiedDate as lastmodi5_4_0_,
            member0_.team_TEAM_ID as team_tea7_4_0_,
            member0_.USERNAME as username6_4_0_,
            team1_.createdBy as createdb2_9_1_,
            team1_.createdDate as createdd3_9_1_,
            team1_.lastModifiedBy as lastmodi4_9_1_,
            team1_.lastModifiedDate as lastmodi5_9_1_,
            team1_.name as name6_9_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.team_TEAM_ID=team1_.TEAM_ID

위의 SQL 문을 보면 Team 클래스의 객체가 LAZY 로 설정되어 있음에도 불구하고 JPQL Fetch Join 으로 인해 Team 테이블까지 한번에 테이블 조인하여 데이터를 가져오는 것을 볼 수 있다.

 

 

@ManyToOne, @OneToOne 은 기본이 즉시 로딩으로 설정되어 있다.

그렇기 때문에 해당 어노테이션 으로 선언한 클래스 객체는 지연 로딩으로 설정해주려면 직접 fetch 속성을 이용해 LAZY 설정을 걸어줘야 한다.

(@OneToMany, @ManyToMany 는 기본이 지연 로딩이다.)

 

 

지연 로딩 활용?

자주 함께 사용하는 클래스들 끼리는 즉시 로딩을 사용하는 편이 좋다고는 하나, 굉장히 이론적인 부분으로 실무에서는 그냥 다 지연 로딩으로 설정해 줘야 한다.

 

그러다가 필요한 경우에만 JPQL Fetch Join 이나 Entity Graph 기능을 활용하여 즉시 로딩 기능을 만들어준다.

 

 

다음 글에서는 영속성 전이(CASCADE) 와 고아 객체에 대해 알아보자.