-> 블로그 이전

[JPA] 즉시로딩 & 지연로딩

2022. 7. 6. 12:43Language`/JPA

 

[JPA] 프록시

<예시> 선수 Entity와 팀 Entity가 존재하고 서로 다대일 관계이다. @Entity @Table(name = "member") public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_i..

cs-ssupport.tistory.com

앞서 배운 "프록시"개념은 주로 연관된 엔티티를 "지연 로딩"할 때 사용한다

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    private String name;
}

 

이전 프록시 포스팅에서 발생했던 문제는 다음과 같다

비즈니스 로직 1)에서는 애초에 로직자체가 회원을 조회할 때 팀도 같이 조회하자는 로직이였다
하지만 비즈니스 로직 2)에서는 회원만 조회하기로 하였는데 원하지 않는 "팀"도 같이 조회가 됨을 확인할 수 있었다

 

이렇게 두 로직이 서로 대립할 때 회원 엔티티와 연관된 "팀 엔티티"에 대한 고민

1) 회원 엔티티를 조회할 때 DB에서 함께 조회하는 것이 맞을까 (즉시 로딩)

2) 아니면 팀 엔티티를 실제 사용하는 시점에 DB에서 조회하는게 맞을까 (지연 로딩)

 

1)의 경우를 "즉시 로딩 전략"이라고 하고, 2)의 경우를 "지연 로딩 전략"이라고 한다

즉시 로딩

즉시 로딩 전략을 사용하려면 연관관계 애노테이션의 fetch 속성을 FetchType.EAGER로 설정하면 된다

@xxxToOne : 기본 로딩 전략 = EAGER
@xxxToMany : 기본 로딩 전략 = LAZY

@~~~ToOne은 기본 전략이 EAGER(즉시 로딩)이고, @~~ToMany는 기본 전략이 LAZY(지연 로딩)이다

 

Example)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

현재 Member는 "즉시 로딩 전략"을 이용해서 Member를 조회할 때 연관된 Team도 그 시점에 전부 로딩하기로 하였다

Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam();
System.out.println("선수 = " + findMember);
System.out.println("소속 팀 = " + findMemberTeam);

System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.team_id as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?
        
선수 = Member{id=1, name='member1'}
소속 팀 = Team{id=1, name='teamA'}

===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team

즉시 로딩 전략을 사용하고 프록시 여부를 확인해보았는데 Member는 당연히 실제 Entity이고, 즉시 로딩에 의해 조회된 Team 또한 실제 Entity이다

>> "즉시 로딩 전략"을 활용하면 연관된 EntityDB에서 조회해서 실제 Entity가 반환된다

대부분의 JPA 구현체는 "즉시 로딩을 최적화"하기 위해서 가능하면 조인 쿼리를 사용한다
>> 위의 예제에서도 Member와 Team간의 left outer join을 통해서 쿼리 1번으로 연관된 두 엔티티를 모두 조회하였다

 

※ 즉시로딩 & NULL 제약 조건

위의 조인을 잘 보면 "left outer join" : 외부조인을 통해서 Member과 Team이 조인을 하고 있다

 

현재 Member Table에서는 Team 컬럼에 대한 nullable을 허용하고 있는 상태이다

>> 따라서 Player중에 team이 없는 Player도 존재한다는 의미이다

 

그렇기 때문에 이 Member에 대한 instance도 가져와야 하기 때문에 내부 조인이 아닌 외부 조인을 하는 것이다

그런데 여기서 당연히 외부 조인보다는 내부 조인이 성능적 이점이 더 크다

>> 따라서 Team에 대한 nullable을 허용하지 않는다면 외부 조인이 아닌 내부 조인이 이루어질 것이다

 

(1) nullable = true

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.team_id as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?

 

(2) nullable = false

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    @ManyToOne
    @JoinColumn(name = "team_id", nullable = false)
    private Team team;
}
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.team_id as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?

 

여기서 @JoinColumn에서 nullable = false를 통해서 내부 조인을 사용하도록 할 수도 있고, 연관관계 애노테이션에 "optional = false"를 사용해도 내부 조인을 사용하도록 할 수 있다

 

(3) optional = false

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    @ManyToOne(optional = false)
    @JoinColumn(name = "team_id")
    private Team team;
}
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.team_id as team_id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.member_id=?

 

따라서 JPA에서의 [외부조인 / 내부조인] 선택 전략은 다음 관계에 의해서 결정된다

→  선택적 관계 = 외부 조인

→  필수적 관계 = 내부 조인

 

<FetchType.EAGER + 조인 전략>
@xxxToOne
- optional = false : 내부 조인
- optional = true : 외부 조인

@xxxToMany
- optional = false : 외부 조인
- optional = true : 외부 조인

@xxxToMany의 경우 컬렉션으로 양방향 매핑을 한 경우이다.

컬렉션 즉시로딩은 무조건 "외부 조인"을 사용한다

왜냐하면 반드시 조회가 되어야 하는 "One"의 경우 만약 컬렉션 내부에 element가 하나도 없다면 조회가 되지 않는 문제가 발생할 수 있다.

>> 그래서 웬만하면 @xxxToMany의 기본 로딩 전략인 LAZY는 변경하지말고 무조건 LAZY로 사용해야 한다


지연 로딩

지연 로딩을 사용하려면 fetch 속성을 FetchType.LAZY로 설정하면 된다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}
Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam();
System.out.println("회원 : " + findMember);
System.out.println("회원 소속 팀 : " + findMemberTeam); // 여기서 프록시 객체가 실제 Team Entity를 가리키게 된다

System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
회원 : Member{id=1, name='member1'}

Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
회원 소속 팀 : Team{id=1, name='teamA'}

===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team$HibernateProxy$I82eho2L

LAZY전략은 실제 사용할 때 DB에서 조회해오는 것이기 때문에 Team을 실제 사용하는 시점에 DB로 select query를 날려서 team을 찾아온다

 

그리고 프록시 여부를 확인해보면 Member는 당연히 실제 Entity가 반환되고, 여기서 실제 Member Entity를 반환할 때 연관된 Team Entity"지연 로딩 전략"을 사용하기 때문에 Team은 "프록시 객체"로 존재하게 된다

 

연관된 엔티티가 이미 영속성 컨텍스트에 존재한다면??

연관된 Team Entity가 "이미 영속성 컨텍스트에 존재"한다면 프록시 객체를 사용할 이유가 없다

따라서 영속성 컨텍스트에 연관된 엔티티가 이미 존재한다면 "지연 로딩 전략"을 사용해도 이미 있기 때문에 실제 Entity로 사용할 수 있다

Team findTeam = em.find(Team.class, 1L); // findMember의 팀을 미리 조회해서 실제 Team Entity를 영속성 컨텍스트에 보관

Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam(); // 이미 영속성 컨텍스트에 존재하기 때문에 프록시가 아닌 실제 Entity
System.out.println("회원 : " + findMember);
System.out.println("회원 소속 팀 : " + findMemberTeam);

System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
System.out.println("findTeam == findMemberTeam ? " + (findTeam.getClass() == findMemberTeam.getClass()));
Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
        
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
        
회원 : Member{id=1, name='member1'}
회원 소속 팀 : Team{id=1, name='teamA'}

===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team
findTeam == findMemberTeam ? true