2023. 02.07 15:40 복습 시작
이번에는 프록시와 즉시 로딩, 지연 로딩에 대해 알아보겠다.
객체는 객체 그래프로 연관된 객체들을 탐색한다.
그런데 객체가 DB에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.
JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다.
프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라,
실제 사용하는 시점에 데이터베이스에서 조회할 수 있다.
하지만 자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회하는 것이 효과적이다.
JPA는 즉시 로딩과 지연 로딩이라는 방법으로 둘을 모두 지원한다.
한 가지 예시를 들어보자.
다대일 단방향 관계인 멤버와 팀이 있다.
즉 멤버는 팀을 참조하는 참조 변수가 있고, 팀은 멤버를 참조하는 변수(List)가 없다. 아래 코드를 살펴보자.
//회원과 팀 함께 출력
public void printUserAndTeam(String memberId){
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름 : " + member.getUsername());
System.out.println("소속팀 : " + team.getName());
}
//회원만 함께 출력
public void printUser(String memberId){
Member member = em.find(Member.class, memberId);
System.out.println("회원 이름 : " + member.getUsername());
}
위 코드와 같이 연관된 엔티티들이 모두 사용되는 것이 아니다.
printUserAndTeam은 팀과 멤버를 모두 출력해야해서 두 엔티티를 모두 조회해야 한다.
그러나 printUser 메소드는 회원 엔티티만 사용하므로, em.find 로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티(Member.team)까지 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않다.
JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데
이것을 지연 로딩이라고 한다.
즉 team.getName()처럼 엔티티의 실제 값을 사용할 때 데이터베이스에서 엔티티를 조회하는 것이다.
이 방법을 사용하면 printUser 메소드는 회원 데이터만 데이터베이스에서 조회해도 된다.
프록시
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
프록시 특징은 다음과 같이 정리할 수 있다.
- 실제 클래스를 상속받아서 만들어진다.
- 실제 클래스와 겉모양이 같다.
- 이론상 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데,
이것을 프록시 객체의 초기화라고 한다.
Member member = em.getReference(Member.class, "memberId");
member.getName();
프록시 초기화 과정은 아래와 같다.
초기화 과정은 다음과 같다.
- 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
- 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라고 한다.
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
- 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
- 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
프록시의 특징은 아래와 같다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능. - 프록시 객체는 원본 엔티티를 상속받은 객체다. 따라서 타입 체크 시 조심해야한다. ( == 보다는, instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. 즉 예외가 터진다.
즉시로딩과 지연로딩
JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지 방법을 제공한다.
- 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
즉시 로딩을 사용하는 예제 코드는 아래와 같다. @ManyToOne fetch 속성을 FetchType.EAGER로 지정한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
실행되는 상황은 아래 이미지와 같다.
즉시 로딩을 하면 Member를 조회 시 항상 Team도 조회한다.
JPA 구현체는 가능하면 조인 쿼리를 사용해서 SQL 쿼리 한 번으로 두 엔티티를 모두 조회한다.
지연로딩을 사용하는 예제 코드는 아래와 같다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //이 부분만 수정.
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
상황은 아래와 같다.
Team team = member.getTeam(); 이 코드를 실시하면 프록시 객체가 들어간다.
team.getName() 이 코드를 실행해야 엔티티를 조회하고 사용한다.
참고할 주의사항은 아래와 같다.
- 그냥 늘 항상 지연 로딩으로 설정하자. 글로벌 지연 로딩 전략.
- 즉시 로딩을 사용하면 예상하지 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne는 기본이 즉시 로딩 -> 지연 로딩으로 수정하자.
- @OneToMany, @ManyToMany는 기본이 지연 로딩.
참고자료
자바 ORM 표준 JPA 프로그래밍 - 김영한
자바 ORM 표준 JPA 프로그래밍 강의 - https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
2023. 02.07 16:20 복습 및 정리 마무리
댓글