[자바 ORM 표준 JPA] JPA 즉시 로딩과 지연로딩

[자바 ORM 표준 JPA] JPA 즉시 로딩과 지연로딩

프록시와 연관관계 관리


즉시 로딩과 지연로딩#


지연 로딩#


Member를 조회할때 Team도 함께 조회해야 할까?#

단순히 Member 정보만 사용하는 비지니스 로직
println(member.getName()); 연관관계가 등록되어 있어도 지금처럼 member만 사용하면 손해일 수 있습니다.

![contact](/images/develop/backend/orm-jpa-basic/eager-loading-and-lazy-loading/im g-001.png)

지연 로딩 LAZY을 사용해서 프록시로 조회#

	@Entity
	public class Member {

		@Id @generatedValue
		private Long id;
		
		@Column(name = "USERNAME")
		private String name;
		
		@ManyToOne(fetch = FetchType.LAZY)
		@JoinColumn(name = "TEAM_ID")
		private Team team;
	}

Member.java

@Entity
public class Member extends BaseEntity{

    public Member(){
    }

    public Member(Long id, String username){
        this.id = id;
        this.username = username;
    }

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY) //Team 객체를 프록시 객체로 조회
    @JoinColumn
    private Team team;

	...
}

JpaMain.java - 애플리케이션 재시작

package relativemapping;

import org.hibernate.Hibernate;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

public class JpaMain {
    //psvm 단축키로 생성 가능
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("relativemapping");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin(); // [트랜잭션] 시작

        try{

            Team team = new Team();
            team.setName("TeamA");
            team.setCreateBy("kim");
            team.setCreateDate(LocalDateTime.now());
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername("MemberA");
            member1.setCreateBy("kim");
            member1.setCreateDate(LocalDateTime.now());
            member1.setTeam(team);
            em.persist(member1);
            Member member2 = new Member();
            member2.setUsername("MemberA");

            member2.setCreateBy("kim");
            member2.setCreateDate(LocalDateTime.now());
            member2.setTeam(team);
            em.persist(member2);


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

            Member m1 = em.find(Member.class, member1.getId());

            System.out.println("m1 = " + m1.getClass());

            tx.commit();

        }catch (Exception e){
            e.printStackTrace();
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }


    private static void printMember(Member member){
        System.out.println("username = "+member.getUsername());
    }

    private static void printMemberAndTeam(Member member){
        String username = member.getUsername();
        System.out.println("username = "+username);

        Team team = member.getTeam();
        System.out.println("team = "+team.getName());
    }
}

console

Hibernate: 
    /* insert relativemapping.Team
        */ insert 
        into
            Team
            (MOD_ID, MOD_DT, REG_ID, REG_DT, NAME, TEAM_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert relativemapping.Member
        */ insert 
        into
            Member
            (MOD_ID, MOD_DT, REG_ID, REG_DT, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert relativemapping.Member
        */ insert 
        into
            Member
            (MOD_ID, MOD_DT, REG_ID, REG_DT, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.MOD_ID as mod_id2_3_0_,
        member0_.MOD_DT as mod_dt3_3_0_,
        member0_.REG_ID as reg_id4_3_0_,
        member0_.REG_DT as reg_dt5_3_0_,
        member0_.TEAM_ID as team_id7_3_0_,
        member0_.USERNAME as username6_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
        
m1 = class relativemapping.Member

Member 엔티티 조회시 Member만 조회 된 것을 확인

JpaMain.java - m1.getTeam().getClass()로 team을 조회 후 Lazy로 가져온 객체 클래스 테스트

            Team team = new Team();
            team.setName("TeamA");
            team.setCreateBy("kim");
            team.setCreateDate(LocalDateTime.now());
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername("MemberA");
            member1.setCreateBy("kim");
            member1.setCreateDate(LocalDateTime.now());
            member1.setTeam(team);
            em.persist(member1);


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

            Member m1 = em.find(Member.class, member1.getId());

            System.out.println("m1 = " + m1.getClass());

            System.out.println("m1.getTeam().getClass() = " + m1.getTeam().getClass());

console

Hibernate: 
    /* insert relativemapping.Team
        */ insert 
        into
            Team
            (MOD_ID, MOD_DT, REG_ID, REG_DT, NAME, TEAM_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert relativemapping.Member
        */ insert 
        into
            Member
            (MOD_ID, MOD_DT, REG_ID, REG_DT, team_TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.MOD_ID as mod_id2_3_0_,
        member0_.MOD_DT as mod_dt3_3_0_,
        member0_.REG_ID as reg_id4_3_0_,
        member0_.REG_DT as reg_dt5_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_,
        member0_.USERNAME as username6_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m1 = class relativemapping.Member
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.MOD_ID as mod_id2_7_0_,
        team0_.MOD_DT as mod_dt3_7_0_,
        team0_.REG_ID as reg_id4_7_0_,
        team0_.REG_DT as reg_dt5_7_0_,
        team0_.NAME as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?


m1.getTeam().getClass() = class relativemapping.Team$HibernateProxy$2xzHCXZv

Process finished with exit code 0

지연 로딩#

contact

지연 로딩 LAZY를 사용해서 프록시로 조회#

contact

Member와 Team을 자주 함께 사용한다면 ?#

contact

Member와 Team을 자주 함께 사용하는 경우 Lazy 로딩을 하게되면 Member한번 Team 한번 쿼리를 각각 계속 호출하기 때문에 비효율적일 수 있습니다.
상황에 맞게 적절히 적용하는게 중요합니다.

즉시 로딩 EAGER를 사용해서 함께조회#

Member.java

package relativemapping;

import javax.persistence.*;


@Entity
public class Member extends BaseEntity{

    public Member(){
    }

    public Member(Long id, String username){
        this.id = id;
        this.username = username;
    }

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER) //**
    @JoinColumn
    private Team team;

	...

JpaMain.java - 애플리케이션 재시작

console

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert relativemapping.Team
        */ insert 
        into
            Team
            (MOD_ID, MOD_DT, REG_ID, REG_DT, NAME, TEAM_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert relativemapping.Member
        */ insert 
        into
            Member
            (MOD_ID, MOD_DT, REG_ID, REG_DT, team_TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.MOD_ID as mod_id2_3_0_,
        member0_.MOD_DT as mod_dt3_3_0_,
        member0_.REG_ID as reg_id4_3_0_,
        member0_.REG_DT as reg_dt5_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_,
        member0_.USERNAME as username6_3_0_,
        team1_.TEAM_ID as team_id1_7_1_,
        team1_.MOD_ID as mod_id2_7_1_,
        team1_.MOD_DT as mod_dt3_7_1_,
        team1_.REG_ID as reg_id4_7_1_,
        team1_.REG_DT as reg_dt5_7_1_,
        team1_.NAME as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.team_TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
        
m1 = class relativemapping.Member

m1.getTeam().getClass() = class relativemapping.Team

Team과 Member가 조인해서 한방 쿼리로 가져오기 때문에 프록시로 가져올 필요가 없기 때문에 일반 엔티티 객체로 가져오게 됩니다.

즉시 로딩#

contact

즉시 로딩(EAGER), Member조회시 항상 Team도 조회#

contact

JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

프록시와 즉시로딩 주의#


  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

즉시 로딩을 적용하면 예상치 못한 SQL이 발생#

Member.java

	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER) //**
    @JoinColumn
    private Team team;

JpaMain - Jpql 예제, console

		   Team team = new Team();
            team.setName("TeamA");
            team.setCreateBy("kim");
            team.setCreateDate(LocalDateTime.now());
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername("MemberA");
            member1.setCreateBy("kim");
            member1.setCreateDate(LocalDateTime.now());
            member1.setTeam(team);
            em.persist(member1);


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

            List<Member> result = em.createQuery( "select m from Member m", Member.class)   //JPQL이란 SQL 그대로 번역하여 Member만 조회, EAGER로 되어있어 Team도 따로 가져와 영속성 컨텍스트 1차 캐시 로딩
                            .getResultList();
                            
            // SQL : select * from Member
            
            // Member.java 의 Team Fetch 타입이 EAGER이기 때문에 Member 객체는 모든 값이 있어야함.
            
            > SQL : select * from Team where TEAM_ID = m.id
                            
                         


            tx.commit();
            
            ------------------
            
            
	Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.MOD_ID as mod_id2_3_,
            member0_.MOD_DT as mod_dt3_3_,
            member0_.REG_ID as reg_id4_3_,
            member0_.REG_DT as reg_dt5_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_.MOD_ID as mod_id2_7_0_,
        team0_.MOD_DT as mod_dt3_7_0_,
        team0_.REG_ID as reg_id4_7_0_,
        team0_.REG_DT as reg_dt5_7_0_,
        team0_.NAME as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
        
            

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

Member.java - FetchType EAGER

	...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private Team team;
	...

JpaMain.java - 2개의 팀과 각각 팀의 Member 생성

            Team teamA = new Team();
            teamA.setName("TeamA");
            teamA.setCreateBy("kim");
            teamA.setCreateDate(LocalDateTime.now());
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("TeamB");
            teamB.setCreateBy("Park");
            teamB.setCreateDate(LocalDateTime.now());
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("MemberA");
            member1.setCreateBy("kim");
            member1.setCreateDate(LocalDateTime.now());
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("MemberB");
            member2.setCreateBy("Park");
            member2.setCreateDate(LocalDateTime.now());
            member2.setTeam(teamB);
            em.persist(member2);

console

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.MOD_ID as mod_id2_3_,
            member0_.MOD_DT as mod_dt3_3_,
            member0_.REG_ID as reg_id4_3_,
            member0_.REG_DT as reg_dt5_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_.MOD_ID as mod_id2_7_0_,
        team0_.MOD_DT as mod_dt3_7_0_,
        team0_.REG_ID as reg_id4_7_0_,
        team0_.REG_DT as reg_dt5_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_.MOD_ID as mod_id2_7_0_,
        team0_.MOD_DT as mod_dt3_7_0_,
        team0_.REG_ID as reg_id4_7_0_,
        team0_.REG_DT as reg_dt5_7_0_,
        team0_.NAME as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?


N+1 문제란 Member 1과 Team이 10 개면 10개의 쿼리가 발생, 100개의 Team이면 100개의 쿼리가 각각 실행
실행한 1개의 쿼리 때문에 N개의 쿼리가 나가게 된다고 하여 N+1 문제라고 합니다.

Member.java - FetchType LAZY

	...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;
	...

console - Member만 조회하는 쿼리 1개 나옴

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.MOD_ID as mod_id2_3_,
            member0_.MOD_DT as mod_dt3_3_,
            member0_.REG_ID as reg_id4_3_,
            member0_.REG_DT as reg_dt5_3_,
            member0_.team_TEAM_ID as team_tea7_3_,
            member0_.USERNAME as username6_3_ 
        from
            Member member0_
2월 03, 2022 11:22:39 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]

Process finished with exit code 0


Team을 사용하지 않기 때문에 Proxy로 Member만 조회합니다. 물론 Team을 루프로 돌리게 되면 각 각 Team을 모두 조회하게 됩니다.

N+1의 대안은 첫번째는 모두 LAZY로 세팅, 뒤에 JPQL에서 배우게 될 Fetch 조인이라는게 있습니다.

Member만 사용할때는 지금과 같이 사용하다가 Team과 같이 사용할땐 Fetch 조인을 하여 한방쿼리로 가져와서 사용합니다.

JpaMain.java - fetch 조인

	...
            List<Member> result = em.createQuery( "select m from Member m join fetch m.team", Member.class)
                            .getResultList();
     ...                    

console

Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.MEMBER_ID as member_i1_3_0_,
            team1_.TEAM_ID as team_id1_7_1_,
            member0_.MOD_ID as mod_id2_3_0_,
            member0_.MOD_DT as mod_dt3_3_0_,
            member0_.REG_ID as reg_id4_3_0_,
            member0_.REG_DT as reg_dt5_3_0_,
            member0_.team_TEAM_ID as team_tea7_3_0_,
            member0_.USERNAME as username6_3_0_,
            team1_.MOD_ID as mod_id2_7_1_,
            team1_.MOD_DT as mod_dt3_7_1_,
            team1_.REG_ID as reg_id4_7_1_,
            team1_.REG_DT as reg_dt5_7_1_,
            team1_.NAME as name6_7_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.team_TEAM_ID=team1_.TEAM_ID


또다른 방법은 EntityGraph 라는 어노테이션을 사용하는 방법과 Batch size 방식이 있습니다.

@ManyToOne 은 기본이 즉시 로딩 EAGER#

contact

기본이 즉시 로딩이기 때문에 모두 @ManyToOne(fetch = FetchType.LAZY) 를 추가해줘야한다 !!!

지연 로딩 활용#


이론적으로는

  • MemberTeam은 자주 함께 사용 -> 즉시 로딩
  • MemberOrder은 가끔 사용 -> 지연 로딩
  • OrderProduct는 자주 함께 사용 -> 즉시 로딩

contact

실무에서는 지연로딩으로 무조건 다 세팅해두어야 합니다…

contact

em.find(Member.class, member1.getId()); 를 통해 조회시 team과는 Join한 한방 쿼리로 조회됩니다.

orders는 지연 로딩을 이용해서 프록시로 들어오게 됩니다.

orders를 사용하여 프록시를 초기화 하게되면

contact

지연 로딩 - 실무#


  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!
  • 즉시 로딩은 상상하지도 못한 쿼리가 나간다.

참고- 자바 ORM 표준 JPA - 김영한#