0%

[JPA] proxy, fetch 전략

1. 들어가며

프록시의 기본과, JPA에서 fetch 전략이 어떤 것들이 있는 지 알아보자.

프록시 기초

PA에서 식별자로 엔티티 하나를 조회할 때 EntityManager.find()를 사용한다. 이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터 베이스를 조회한다.

1
Member member = em.find(Member.class, "member1");

만약에 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManger.getReference()를 사용한다.

1
Member member = em.getReference(Member.class, "member1");

이 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

즉시로딩 시, JPA는 외부조인(Left Outer) 조인을 사용한다. 그 이유는 내부조인을 사용하게 되면, 외래키 null값이 존재하는 경우, 우리가 원하는 어떤 데이터도 조회할 수 없기 때문이다. 성능적인 측면에서 내부 조인이 좋기 때문에. 내부조인을 사용할려면, 외래키를 not null 조건으로 특정해야 한다

1
2
3
4
5
6
7
8
@Entity
public class Member {
//...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="TEAM_ID", nullable = false)
private Team team;
//...
}

2. 예제코드

위의 말이 정확히 이해가 가지 않아서 테스트 케이스를 작성해 보자.
간단한 Team(1): Member(N) 관계를 갖고 (양방향 관계) 매핑을 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@Entity
@Table(name = "TEAM")
public class Team {

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

@Column(name = "NAME")
private String name;

@OneToMany
private List<Member> memberList = new ArrayList<>();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@Entity
@Table(name = "MEMBER")
public class Member {

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

@Column(name = "NAME")
private String name;

@Column(name = "AGE")
private Integer age;

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
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
@Test
public void teamTest() {

//Given
Member member = new Member();
member.setName("andrew");
member.setAge(32);

Team team = new Team();
team.setName("A팀");

//연관관계 설정
member.setTeam(team);
team.getMemberList().add(member);

teamRepository.save(team);
memberRepository.save(member);

//When
Member existMember = memberRepository.findById(member.getId()).get();

//Then
Assert.assertEquals(existMember.getTeam().getName(), "A팀");

}

이 테스트는 Member, Team 객체를 저장 하고, 객체 그래프 탐색하는 테스트 케이스 입니다.

위에서 언급했듯이, 즉시 로딩한 경우, left outer join 을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select
member0_.member_id as member_i1_1_0_,
member0_.age as age2_1_0_,
member0_.name as name3_1_0_,
member0_.team_id as team_id4_1_0_,
team1_.team_id as team_id1_2_1_,
team1_.name as name2_2_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?

JPA 에서 기본 Fetch 전략

즉시 로딩인지 아닌지 알아 보기 위해서 fetch 옵션을 살펴보자. JPA에서는 기본 fetch 전략은 다음과 같다.

  • @OneToMany // fetch = FetchType.LAZY
  • @ManyToOne // fetch = FetchType.EAGER

연관 관계가 1:N 으로 매핑이 되면, 당연히 즉시 로딩하게 되면, 매번 조인 쿼리가 발생하기 때문에 성능에 안 좋은 영향을 미친다. 그와는 반대로 N:1 은 즉시 로딩이 기본 전략이다.

위의 예제에서는 Member(N): Team(1) 이기 때문에 즉시 로딩이 된다.

이번에는 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
@Test
public void lazy로딩_teamTest() {

//Given
Member member = new Member();
member.setName("andrew");
member.setAge(32);

Team team = new Team();
team.setName("A팀");

//연관관계 설정
member.setTeam(team);
team.getMemberList().add(member);

teamRepository.save(team);
memberRepository.save(member);

//When
Team existTeam = teamRepository.findById(team.getId()).get();

//Then
Assert.assertEquals(existTeam.getMemberList().get(0).getName(), "andrew");

}

다음과 같은 Exception이 발생한다.

1
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.demo.spring.jpa.team.Team.memberList, could not initialize proxy - no Session

해결방법은

  • fetchType을 earger로 변경
  • join fetch

두 개가 같은 쿼리를 만드나..?

TODO

  • QueryDSL로 쿼리를 짜보면서, 어떤 차이가 있는지 확인할 예정

Welcome to my other publishing channels