개요
Member와 Team 엔티티가 존재한다고 가정해보자
만약 Member를 조회할 때 Team도 함께 조회를 해야할까?
결론부터 말하자면 JPA는 Member만 조회하고 싶을 때 Team 객체를 프록시 객체로 넣어주는 기능을 지원한다.
어떤 방법을 동작하는지 알아보자.
1. 프록시
- 프록시 기초
- 프록시(Proxy)는 객체 지향 프로그래밍에서 실제 객체를 대신해서 대리 역할을 수행하는 객체다.
- JPA에서 엔티티 조회시 즉시로딩이 아닌 지연로딩으로 설정하면 필요할 때만 로딩하도록 엔티티 내부에 존재하는 연관 객체에 프록시 객체를 넣어준다.
- 프록시 특징
- 프록시 객체는 실제 클래스를 상속 받아서 만들어진다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출한다.
- 프록시 객체는 처음 사용할 때 한번만 초기화한다.
- 프록시 객체를 초기화할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다.( == 비교 연산자 사용X, instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReferece( )를 호출해도 실제 엔티티 반환한다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화하면 LazyInitialzationException 발생한다
- entityManger.find( ) : 데이터베이스를 통해서 실제 엔티티 객체 조회
- entityManger.getReference( ): 데이터베이스 조회를 미루는 가짜(프록시) 객체 조회
코드를 통해 알아보자
Member findMember = em.getReference(Member.class, member1.getId());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
getReferece는 프록시 객체를 조회하는 메서드이다. 실행시 findMember는 프록시 객체가 된다. 객체안에 존재하는 필드에 접근하게 되면 그때 영속성 컨텍스트를 조회 후 없으면 데이터베이스에서 조회를 한다.
그러면 실행결과는 다음과 같다.
findMember.getTeam().getClass() = class jpa.practice.v0.Team$HibernateProxy$W16X4OfW
Hibernate:
select
t1_0.id,
t1_0.name
from
team t1_0
where
t1_0.id=?
findMember.getTeam().getName() = teamA
findMember.getTeam().getClass() = class jpa.practice.v0.Team$HibernateProxy$W16X4OfW
여기서 알 수 있는점은 프록시 객체의 필드에 접근하면 그때 실제 엔티티를 조회한다는 점과
프록시 객체가 실제 엔티티를 호출해도 실제 엔티티로 변하는게 아닌 프록시 객체로 남아있다는 점이다.
프록시 객체를 통해서 실제 엔티티에 접근한다는 뜻이다.
2. 지연로딩(Lazy Loading)
지연로딩(lazy loading)은 JPA에서 연관된 엔티티를 필요한 시점에 조회하도록 하는 데이터 로딩 전략이다. 즉, 실제로 데이터를 사용할 때 까지 데이터베이스에서 가져오지 않고 프록시 상태로 두는 방식이다.
지연 로딩 특징
- 필요한 데이터만 조회
- 연관된 데이터를 즉시 가져오지 않고, 해당 데이터를 실제로 사용할 때 데이터베이스에서 조회한다
- 프록시 객체 사용
- 프록시 객체를 생성하여 연관된 객체를 프록시 객체로 반환한다.
코드 예제
@Entity
class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정
private Team team; // Team은 연관 엔티티
}
@Entity
class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
// 실행 코드
Member member = entityManager.find(Member.class, memberId); // Member만 조회
System.out.println(member.getName()); // Member의 데이터는 즉시 사용 가능
Team team = member.getTeam(); // Team은 아직 프록시 상태
System.out.println(team.getName()); // 이 시점에 Team을 초기화하며 DB 조회
지연 로딩 설정
- @ManyToOne 및 @OneToOne 기본값: FetchType.EAGER
- 하지만 대부분의 경우 명시적으로 FetchType.LAZY를 설정해주는 것이 좋다.
- @OneToMany 및 @ManyToMany 기본값: FetchType.LAZY
지연 로딩 주의점
- N+1 문제:
- 지연 로딩이 잘못 설계되면 데이터베이스에 너무 많은 쿼리가 발생할 수 있다.
- 예: 연관된 엔티티를 반복적으로 조회하면 N+1 쿼리가 실행된다.
- 해결책: Fetch Join이나 배치 사이즈(batch size) 설정.
- LazyInitializationException:
- 프록시 객체가 초기화되지 않은 상태에서 영속성 컨텍스트를 벗어나면 데이터베이스를 조회할 수 없어 예외가 발생한다
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
em.clear(); // 영속성 컨텍스트 비움
System.out.println("========================================================");
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
tx.commit();
영속성 컨텍스트를 비우고 프록시 객체에 접근해봤다.
'org.hibernate.LazyInitializationException'이 발생한 것을 볼 수 있다..
3. 즉시 로딩 (Eager Loading)
즉시 로딩(Eager Loading)은 JPA에서 연관된 엔티티를 즉시 로딩하는 데이터 로딩 전략이다. 즉, 엔티티를 조회할 때 연관된 모든 데이터도 한번에 데이터베이스에서 가져온다. 즉시 로딩은 지연 로딩과 반대되는 개념으로 연관된 엔티티를 함꼐 로드하여 즉시 사용 가능하도록 한다.
즉시 로딩 특징
- 연관된 모든 데이터를 즉시 조회:
- 주 엔티티와 연관된 엔티티를 함께 조회하기 때문에 연관된 데이터를 바로 사용할 수 있다.
- 쿼리의 복잡성 증가:
- 연관된 엔티티를 모두 조회하는 쿼리가 실행되기 때문에 쿼리 자체가 복잡해지고 조회 성능에 영향을 줄 수 있다.
코드 예제
@Entity
class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 설정
private Team team; // Team은 연관 엔티티
}
@Entity
class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
// Member 조회 시점에 Team도 함께 조회
Member member = entityManager.find(Member.class, memberId);
Team team = member.getTeam(); // 이미 로딩된 상태이므로 DB 추가 조회 필요 없음
join이 되어 한번에 실행된 것을 볼 수 있다.
즉시 로딩 주의점
- 비효율적인 데이터 조회:
- 연관된 엔티티가 많은 경우 불필요한 데이터도 함께 로딩되므로 메모리와 네트워크 비용이 증가한다.
- N+1 문제:
- Eager Loading을 남발할 경우, 연관된 데이터를 모두 로드하는 과정에서 N+1 문제가 발생할 수 있다.
List<Member> members = entityManager.createQuery("SELECT m FROM Member m", Member.class).getResultList();
for (Member member : members) {
System.out.println(member.getTeam().getName()); // 즉시 로딩으로 인해 Team 조회 쿼리가 여러 번 발생 가능
}
실행결과는 다음과 같이 N+1문제가 발생한다.
해결 방법
즉시 로딩을 지연 로딩으로 변경하거나 fetch join을 사용하면 된다.
// fetch join으로 변경
List<Member> resultList = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
for (Member member : resultList) {
System.out.println("member.getUsername() = " + member.getUsername());
System.out.println("member.getTeam() = " + member.getTeam());
}
그럼 아래와 같이 N+1문제가 발생하지 않고 join으로 한번에 불러오는 것을 볼 수 있다.
'JPA' 카테고리의 다른 글
[JPA] 연관관계 매핑 (0) | 2024.11.25 |
---|---|
[JPA] 엔티티 매핑 (0) | 2024.11.25 |
[JPA] 영속성 전이 CASCADE, OrphanRemoval = true 이해 및 차이 (0) | 2024.11.19 |
[JPA] JPA와 영속성 컨텍스트란? (1) | 2024.11.18 |