Post

JPA 연관관계 Fetch 전략

🎬 Intro

JPA 연관관계 Fetch 전략에 대해 알아봅시다.

✅ 지연 로딩(Lazy Loading)

필요한 시점에 DB에서 데이터를 불러오는 기능입니다. JPA는 엔티티를 우선 프록시 객체로 가져온 뒤 필요한 시점에 DB에 SELECT 쿼리를 날려 가져오게 됩니다.

여기서 주의 할 점은 이로 인해서 원치 않게 프록시 객체만큼 SELECT 쿼리가 날라가게 되는데요. 이를 N + 1 문제라고 합니다. 자세한 내용은 다른 포스트에서 다루도록 하겠습니다.

✅ 즉시 로딩(Eager Loading)

즉시 로딩은 그 연관된 엔티티들을 즉시 로딩하여, 추가적인 DB 접근을 최소화합니다. 따라서 연관된 데이터를 한 번에 처리하기에 적합하지만, 필요하지 않는 데이터를 로딩하기 위해 불필요한 Join등이 발생할 수 있어 비효율적일 수 있습니다.

📝 예제

Lazy 전략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Entity
@Table
@NoArgsConstructor
@Getter
@Setter
public class Member {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(name = "member_id")
  private Long id;

  private String name;


  // Join Column을 기준으로 Team의 객체를 찾아오게 된다.
  // 만약 Member 테이블에 team_id가 null 이면 Team 객체도 null이다.
  // @ManyToOne은 Fetch 기본 전략이 EAGER
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "team_id")
  private Team team;

  @Builder
  public Member(final Long id, final String name, final Team team) {
    this.team = team;
    this.id= id;
    this.name = name;
  }


}



@Entity
@Table
@NoArgsConstructor
@Getter
@Setter
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(name = "team_id")
  private Long id;

  private String name;

  // mappedBy에는 Member에 존재하는 Team의 객체 이름을 넣어줍니다.
  // @OneToMany 는 Fetch 기본전략이 LAZY
  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
  private List<Member> members = new ArrayList<>();

  @Builder
  public Team(final Long id, final String name) {

    this.id = id;
    this.name = name;
  }

  /*
  연관관계 편의 메서드, Member와 Team 중 한곳에서만 선언한는것이 좋습니다.
   */
  public void addMember(Member member) {
    member.setTeam(this);
    members.add(member);
  }

}

  • Member, Team 은 양방향 연관관계 입니다.
  • Member, Team 모두 Fetch는 Lazy 전략 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@DataJpaTest
// 실제 DB 사용 옵션, 해당 애노테이션이 없을 시 @DataJpaTest는 스프링부트의 테스트 전용 DB를 사용하게 됨
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class JpaTest7 {

  @Autowired
  MemberRepository memberRepository;

  @Autowired
  TeamRepository teamRepository;


  @Test
  @DisplayName("Lazy 전략 테스트")
  void lazyTest() throws Exception {

    //given
    //when
    final Member member = memberRepository.findById(1L).orElse(null);
    final Team team = member.getTeam();

    //then
    assertThat(team).isInstanceOf(HibernateProxy.class);
    System.out.println("========= 초기화 ==========");
    Hibernate.initialize(team);
    assertThat(team).isInstanceOf(HibernateProxy.class);

  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Hibernate] 
    select
        m1_0.member_id,
        m1_0.name,
        m1_0.team_id 
    from
        member m1_0 
    where
        m1_0.member_id=?
========= 초기화 ==========
[Hibernate] 
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?

  • Member의 Fetch 전략이 Lazy이므로 영속성 1차 캐시에 team객체가 프록시 객체 형태로 저장되어 있습니다.
  • Hibernate.initialize() 메서드를 통해 team 프록시 객체를 초기화 합니다. 이때 DB에 SELECT 쿼리가 실행됩니다.
  • 초기화 되더라도 team객체는 여전히 프록시 객체로 남아있습니다.그러나 데이터는 채워져 있는 상태입니다.

Eager 전략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Entity
@Table
@NoArgsConstructor
@Getter
@Setter
public class Member {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(name = "member_id")
  private Long id;

  private String name;


  // Join Column을 기준으로 Team의 객체를 찾아오게 된다.
  // 만약 Member 테이블에 team_id가 null 이면 Team 객체도 null이다.
  // @ManyToOne은 Fetch 기본 전략이 EAGER
  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "team_id")
  private Team team;

  @Builder
  public Member(final Long id, final String name, final Team team) {
    this.team = team;
    this.id= id;
    this.name = name;
  }


}

@Entity
@Table
@NoArgsConstructor
@Getter
@Setter
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(name = "team_id")
  private Long id;

  private String name;

  // mappedBy에는 Member에 존재하는 Team의 객체 이름을 넣어줍니다.
  // @OneToMany 는 Fetch 기본전략이 LAZY
  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
  private List<Member> members = new ArrayList<>();

  @Builder
  public Team(final Long id, final String name) {

    this.id = id;
    this.name = name;
  }

  /*
  연관관계 편의 메서드, Member와 Team 중 한곳에서만 선언한는것이 좋습니다.
   */
  public void addMember(Member member) {
    member.setTeam(this);
    members.add(member);
  }

}


  • Member의 Fetch 전략을 EAGER로 변경하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@DataJpaTest
// 실제 DB 사용 옵션, 해당 애노테이션이 없을 시 @DataJpaTest는 스프링부트의 테스트 전용 DB를 사용하게 됨
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class JpaTest8 {

  @Autowired
  MemberRepository memberRepository;

  @Autowired
  TeamRepository teamRepository;

  @Test
  @DisplayName("Eager 전략 테스트")
  void eagerTest() throws Exception {

    //given
    //when
    final Member member = memberRepository.findById(1L).orElse(null);
    final Team team = member.getTeam();

    //then
    assertThat(team).isNotInstanceOf(HibernateProxy.class);

  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Hibernate] 
    select
        m1_0.member_id,
        m1_0.name,
        t1_0.team_id,
        t1_0.name 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?

  • Member의 Team 매핑 전략이 Eager이므로 조회 시 left join으로 team 정보도 함께 가져옵니다.
  • 따라서 team은 프록시 객체가 아니라 실제 객체가 됩니다.

✅ 지연 로딩 vs 즉시 로딩

결론적으로, 즉시 로딩은 모든 연관된 엔티티를 즉시 로드하여 추가적은 DB 접근을 줄이는 장점이 있지만, 경우에 따라 불필요한 데이터까지 로드하게 되어 성능 저하가 발생할 수 있습니다. 반면에 지연 로딩은 필요한 시점에만 데이터를 로드할 수 있지만 N + 1문제가 존재합니다. 어떤 로딩 방식을 사용할지는 애플리케이션의 요구 사항과 성능 고려 사항에 따라 결정해야 합니다.

This post is licensed under CC BY 4.0 by the author.