들어가며
프록시의 기본과, JPA에서 fetch 전략이 어떤 것들이 있는 지 알아보자.
프록시 기초
PA에서 식별자로 엔티티 하나를 조회할 때 EntityManager.find()
를 사용한다. 이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터 베이스를 조회한다.
Member member = em.find(Member.class, "member1");
만약에 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManger.getReference()
를 사용한다.
Member member = em.getReference(Member.class, "member1");
이 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
즉시로딩 시, JPA는 외부조인(Left Outer) 조인을 사용한다. 그 이유는 내부조인을 사용하게 되면, 외래키 null값이 존재하는 경우, 우리가 원하는 어떤 데이터도 조회할 수 없기 때문이다. 성능적인 측면에서 내부 조인이 좋기 때문에. 내부조인을 사용할려면, 외래키를 not null 조건으로 특정해야 한다
@Entity
public class Member {
//...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="TEAM_ID", nullable = false)
private Team team;
//...
}
2. 예제코드
위의 말이 정확히 이해가 가지 않아서 테스트 케이스를 작성해 보자.
간단한 Team(1): Member(N) 관계를 갖고 (양방향 관계) 매핑을 해보자.
@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<>();
}
@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;
}
@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 을 한다.
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=?
2.1. JPA 에서 기본 Fetch 전략
즉시 로딩인지 아닌지 알아 보기 위해서 fetch 옵션을 살펴보자. JPA에서는 기본 fetch 전략은 다음과 같다.
- @OneToMany // fetch = FetchType.LAZY
- @ManyToOne // fetch = FetchType.EAGER
연관 관계가 1:N 으로 매핑이 되면, 당연히 즉시 로딩하게 되면, 매번 조인 쿼리가 발생하기 때문에 성능에 안 좋은 영향을 미친다. 그와는 반대로 N:1 은 즉시 로딩이 기본 전략이다.
위의 예제에서는 Member(N): Team(1) 이기 때문에 즉시 로딩이 된다.
이번에는 lazy 로딩으로 테스트 해보자
@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이 발생한다.
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
두 개가 같은 쿼리를 만드나..?
📚 Related Posts
- JPA - 값 타입(6)
- JPA - 프록시와 연관관계(5)
- JPA - 다양한 연관관계 매핑(4)
- JPA - 연관관계 매핑(기초)(3)
- JPA - 엔티티 매핑(2)
- JPA - 영속성 관리(1)
- [JPA] 연관관계 매핑 (연관관계 편의 메서드)
- [JPA] 엔티티 설계시 주의사항들
- [JPA] Auditing 사용하기
- [JPA]@Transactional를 통한 Optimization
- [JPA] 엔티티 일부 데이터만 조회하는 Projection
- [JPA] save메서드로 살펴보는 persist와 merge 개념
- [JPA] 쿼리메서드(Lookup 전략)
- [JPA] QueryDSL 설정방법
- [JPA] null 처리
- [JPA] 연관관계 매핑 기초(다대일, 연관관계 주인)
- SpringBoot, JPA, H2를 이용한 간단한API 작성
- [JPA] proxy, fetch 전략
- [JPA] 도메인 클래스 컨버터란?
- [JPA] Custom Repository 만들기
- [JPA] Casecade 옵션