LazyLoading

XingXi·2024년 1월 4일
0

JPA

목록 보기
17/23
post-thumbnail

🍕 Reference

자바 ORM 표준 JPA 프로그래밍 : 교보문고
자바 ORM 표준 JPA 프로그래밍 - 기본편 : 인프런

BaseEntity

@MappedSuperclass
public class BaseEntity
{
    @Id @GeneratedValue
    private Long id;
    private LocalDateTime createDate;
    private LocalDateTime modifyDate;
    private String createUser;
    private String modifyUser;
    private String name;
// Getter setter 생략
...
}

Member

@Entity
public class Member extends BaseEntity
{
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

Team

@Entity
public class Team extends BaseEntity
{

}

다음과 같은 객체 관계에서 Member 를 조회하면 항상 Team 까지 조회가 될 것이다.
Application 에 로직에 따라서 Team 의 정보가 필요 없는 경우에도 굳이 들고와야 한다.
이후 해당 어플리캐이션 사이즈가 커져서 Member객체 안에 Team 을 포함한 많은 연관관계가 생기면 그때도 Member하나 조회하겠다고 연관관계 인 다른 객체들을 모두 들고 와야하는
비용이 발생한다. 이를 위해서 JPA 에서 Proxy 객체를 활용한 지연로딩을 제공한다.

LazyLoading

자원을 필요로 하는 시점에 객체를 로드하는 기술
연관 관계의 Entity 가 실제로 필요한 시점에 로딩

지연 로딩 fetch = FetchType.LAZY

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

연관관계 어노테이션의 fetch속성에 사용한다.
선언할 경우 실제 Entity 가 아닌 Proxy 객체가 생성된다.

해당 객체만 불러오는 기술 여기에서는 Member에 대한 것만 호출

🍇 예시

            Member m = em.find(Member.class, member.getId());
            System.out.println("Member Class Type : "+m.getClass());
            System.out.println("Team of Member's Class type : "+m.getTeam().getClass());
            System.out.println("Proxy Initialize");
            System.out.println("m.getTeam().getName() : "+m.getTeam().getName());
결과
---
Member Class Type : class org.example.entity.Member
Team of Member's Class type : class org.example.entity.Team$HibernateProxy$IHPqMI70
Proxy Initialize
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.createDate as createDa2_1_0_,
        team0_.createUser as createUs3_1_0_,
        team0_.modifyDate as modifyDa4_1_0_,
        team0_.modifyUser as modifyUs5_1_0_,
        team0_.name as name6_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
m.getTeam().getName() : A Team

fetch = FetchType.LAZY 로 선언된 Team 객체 타입은 Proxy객체 타입이고,
Team 객체 필요 시 m.getTeam().getName() Team 조회 Query가 발생한다.
LazyLoading 을 통해 연관관계 객체 필요 시 에만 호출하여 사용할 수 있고 이에 따라 비용을 줄일 수 있다.

장점 : 비용 절감

이와 대조되게 연관관계인 객체 모두를 들고오는 기능도 제공한다.

즉시 로딩 fetch = FetchType.EAGER

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

연관관계에 있는 모든 객체들을 한번에 조회하여 영속화 한다
쉽게 말해서 연관있는 모든객체를 한번에 다 가져온다

🍇 예시

     Member m = em.find(Member.class, member.getId());
            System.out.println("Member Class Type : "+m.getClass());
            System.out.println("Team of Member's Class type : "+m.getTeam().getClass());
---
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.createDate as createDa2_0_0_,
        member0_.createUser as createUs3_0_0_,
        member0_.modifyDate as modifyDa4_0_0_,
        member0_.modifyUser as modifyUs5_0_0_,
        member0_.name as name6_0_0_,
        member0_.TEAM_ID as TEAM_ID7_0_0_,
        team1_.id as id1_1_1_,
        team1_.createDate as createDa2_1_1_,
        team1_.createUser as createUs3_1_1_,
        team1_.modifyDate as modifyDa4_1_1_,
        team1_.modifyUser as modifyUs5_1_1_,
        team1_.name as name6_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?
Member Class Type : class org.example.entity.Member
Team of Member's Class type : class org.example.entity.Team

Team 객체 타입 또한 Entity 인것을 볼 수 있다.

즉시 로딩 사용은 지양해야..

1. 필요 없는 JOIN Query 발생

즉시 Loading 은 객체와 연관관계인 모든 객체를 한꺼번에 조회하여 영속화한다.
연관관계가 많은 객체의 경우 조회 시 연관된 객체들 모두 조회하여 필요 없는 Query 들이 발생한다.

@Entity
public class Team2 extends BaseEntity
{}

X 3

@Entity
public class Member extends BaseEntity
{
//    @ManyToOne(fetch = FetchType.LAZY)
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM2_ID")
    private Team2 team2;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM3_ID")
    private Team3 team3;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM4_ID")
    private Team4 team4;

main 메소드 실행 결과

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.createDate as createDa2_0_0_,
        member0_.createUser as createUs3_0_0_,
        member0_.modifyDate as modifyDa4_0_0_,
        member0_.modifyUser as modifyUs5_0_0_,
        member0_.name as name6_0_0_,
        member0_.TEAM_ID as TEAM_ID7_0_0_,
        member0_.TEAM2_ID as TEAM8_0_0_,
        member0_.TEAM3_ID as TEAM9_0_0_,
        member0_.TEAM4_ID as TEAM10_0_0_,
        team1_.id as id1_1_1_,
        team1_.createDate as createDa2_1_1_,
        team1_.createUser as createUs3_1_1_,
        team1_.modifyDate as modifyDa4_1_1_,
        team1_.modifyUser as modifyUs5_1_1_,
        team1_.name as name6_1_1_,
        team2x2_.id as id1_2_2_,
        team2x2_.createDate as createDa2_2_2_,
        team2x2_.createUser as createUs3_2_2_,
        team2x2_.modifyDate as modifyDa4_2_2_,
        team2x2_.modifyUser as modifyUs5_2_2_,
        team2x2_.name as name6_2_2_,
        team3x3_.id as id1_3_3_,
        team3x3_.createDate as createDa2_3_3_,
        team3x3_.createUser as createUs3_3_3_,
        team3x3_.modifyDate as modifyDa4_3_3_,
        team3x3_.modifyUser as modifyUs5_3_3_,
        team3x3_.name as name6_3_3_,
        team4x4_.id as id1_4_4_,
        team4x4_.createDate as createDa2_4_4_,
        team4x4_.createUser as createUs3_4_4_,
        team4x4_.modifyDate as modifyDa4_4_4_,
        team4x4_.modifyUser as modifyUs5_4_4_,
        team4x4_.name as name6_4_4_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    left outer join
        Team2 team2x2_ 
            on member0_.TEAM2_ID=team2x2_.id 
    left outer join
        Team3 team3x3_ 
            on member0_.TEAM3_ID=team3x3_.id 
    left outer join
        Team4 team4x4_ 
            on member0_.TEAM4_ID=team4x4_.id 
    where
        member0_.id=?

Member 객체 하나 조회하겠다고 이런 대량의 코드들이 발생한다.

2. N + 1 문제 발생

FetchType.EAGER 를 사용하고 JPQL을 사용할 때 발생한다 .

🍇 예시

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

다음 코드가 실행될 경우

----
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_0_,
            member0_.createDate as createDa2_0_,
            member0_.createUser as createUs3_0_,
            member0_.modifyDate as modifyDa4_0_,
            member0_.modifyUser as modifyUs5_0_,
            member0_.name as name6_0_,
            member0_.TEAM_ID as TEAM_ID7_0_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.createDate as createDa2_1_0_,
        team0_.createUser as createUs3_1_0_,
        team0_.modifyDate as modifyDa4_1_0_,
        team0_.modifyUser as modifyUs5_1_0_,
        team0_.name as name6_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

조회 Query 가 두번 발생하는 것을 볼 수 있다.
위의 JPQL 실행 시 select m from Member m가 먼저 실행되고 영속성 컨텍스트에 할당된다. 할당될때 Team 객체와의 연관관계가 FetchType.EAGER 인 것을 확인하여 다시 Team 객체를 조회하는 Query 가 발생한여 N+1 문제가 생긴다.
시간 순서대로 풀어보면
1. JPQL 실행 시 select m from Member m가 먼저 실행
2. Member객체가 할당될 때 Team 객체와의 연관관계가 FetchType.EAGER 인 것을 확인
3. Team 객체를 조회

추가

@ManyToOne, @OneToOne 은 기본이 즉시로딩이기 때문에 Lazy 로 설정하는 것이 좋다.
@ManyToOne

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ManyToOne {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default FetchType.EAGER;
    boolean optional() default true;
}

@OneToOne

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OneToOne {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default FetchType.EAGER;
    boolean optional() default true;
    String mappedBy() default "";
    boolean orphanRemoval() default false;
}

0개의 댓글