JPA는 어떻게 지연 로딩(Lazy Loading)을 사용할 수 있을까요?
이번 글은 JPA의 프록시에 대해 알아보겠습니다.
프록시가 무엇일까요?
프록시
프록시는 실제 엔티티 객체 대신 데이터베이스 조회를 지연시킬 수 있는 가짜 객체입니다.
데이터베이스 조회를 지연시켜야 하는 이유가 뭘까요?
데이터베이스 조회를 지연시키지 않은 경우엔 하나의 엔티티를 조회할때, 그 엔티티와 연관된 다른 엔티티들의 데이터도 함께 조회하게 됩니다.
만약, 조회한 엔티티만 사용되고 연관된 다른 엔티티들은 필요하지 않은 경우, 실제로 사용되지 않는 데이터까지 불필요하게 조회하게 됩니다. 이능 성능 저하로 이어질 수 있고, 불필요한 리소스 낭비를 초래할 수 있습니다.
이 상황을 코드를 통해 알아보겠습니다.
Member엔티티와 Team엔티티가 다대일 관계를 맺고 있습니다.
Member 엔티티
package com.example.spring_practice.entity;
import jakarta.persistence.*;
import lombok.NoArgsConstructor;
@Entity
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
Team team;
public Member(String name){
this.name = name;
}
public void setTeam(Team team) {
this.team = team;
team.addMember(this);
}
}
Team 엔티티
package com.example.spring_practice.entity;
import jakarta.persistence.*;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
List<Member> members = new ArrayList<>();
public void addMember(Member member){
this.members.add(member);
}
}
H2 DB에 데이터도 저장되어 있는 상태입니다.
팀에 대한 정보를 얻고자 아래와 같이 테스트 코드를 작성합니다.
@SpringBootTest
public class ProxyTest {
@PersistenceContext
private EntityManager em;
@Test
void printTeam() throws Exception {
//given
Team findTeam = em.find(Team.class, 1L);
System.out.println(findTeam.getId());
}
}
테스트 코드에서 출력하고자 하는 정보는 팀의 아이디 입니다.
데이터베이스 지연 조회를 하지 않는 경우엔 팀의 아이디 정보 뿐만 아니라, Team 엔티티 필드에 선언되어 있는 Member 객체에 대한 정보도 함께 가져오게 됩니다. 즉, 필요하지 않은 정보를 위한 조회가 발생한 것입니다.
(단, 테스트시에는 기존 지연로딩을 하던 코드를 즉시 로딩으로 변경하였습니다.)
출력된 쿼리문은 다음과 같습니다.
Member엔티티에 대한 정보가 필요하지 않는데도 left join 쿼리를 사용해서 Member에 대한 정보를 가져옵니다.
JPA는 이러한 문제를 해결하기 위해서 엔티티가 사용되는 시점에 데이터베이스로 조회를 지연하는 방법인 지연 로딩을 제공합니다.
지연 로딩을 사용하기 위해선 가짜 객체인 프록시 객체가 필요합니다.
프록시 객체
프록시 객체는 엔티티가 실제 사용되는 시점까지 데이터베이스 조회를 미루기 위해 사용되는 객체입니다.
프록시 객체는 getReference메서드를 사용해서 인스턴스화 할 수 있습니다.
@Test
@Transactional
void proxyObject() throws Exception {
//given
Member proxyMember = em.getReference(Member.class,1L);
//then
System.out.println("proxyTeamObj = " + proxyMember.getClass());
}
출력한 결과는 아래와 같습니다.
객체 클래스 이름을 보면 $HibernateProxy$FqZHJpjn로 되어있고, HibernateProxy라고 명시되어 있습니다.
프록시 객체에 대해서 알아봤습니다. JPA에서 프록시 객체는 어떤 특징을 가질까요?
프록시 객체의 특징
프록시 객체는 다음과 같은 특징을 가집니다.
- 처음 사용할 때 한번만 초기화 됩니다.
- 프록시 객체를 초기화 한다고 해서 프록시 객체가 실제 엔티티로 바뀌지 않습니다. 실제 엔티티에 접근할 수 있을 뿐입니다.
- 영속성 컨텍스트에 조회하려는 엔티티가 이미 존재하는 경우 getReference()메서드를 호출해서 프록시 객체가 아닌 영속 상태인 실제 엔티티가 반환합니다.
- 프록시 초기화는 영속성 컨텍스트에서 도와주기 때문에 영속화가 되지 않은 준영속 상태에서 프록시를 초기화하면 LazyInitializationException 예외가 발생합니다.
프록시 객체의 초기화는 무엇일까요?
프록시 객체의 초기화
Member 엔티티를 프록시 객체를 코드로 작성하면 다음과 같습니다.
(단, 실제 프록시 객체와 완전히 일치하지 않습니다.)
package com.example.spring_practice.entity;
import org.springframework.cglib.proxy.LazyLoader;
public class MemberProxy extends Member {
private final Long id;
private final LazyLoader lazyLoader;
private boolean initialized = false;
private Member target; // 실제 엔티티를 담을 변수
public MemberProxy(Long id, LazyLoader lazyLoader) {
this.id = id;
this.lazyLoader = lazyLoader;
}
private void initialize() {
if (!initialized) {
this.target = lazyLoader.load(id);
this.initialized = true;
}
}
@Override
public String getName() {
initialize();
return target.getName();
}
@Override
public Team getTeam() {
initialize();
return target.getTeam();
}
}
프록시 객체는 실제 객체(target)에 대한 참조를 필드 변수로 보관합니다.
프록시 객체의 메소드를 호출하는 경우 프록시 객체는 실제 객체(target)의 메소드를 호출하는 방식으로 동작합니다.
데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 과정을 프록시 객체의 초기화라고 합니다.
프록시 객체를 초기화를 하는 방법은 아래 코드와 같습니다.
@Test
@Transactional
void proxyObject() {
//given
Member proxyMember = em.getReference(Member.class,1L);
//then
System.out.println("proxyTeamObj = " + proxyMember.getClass());
System.out.println(proxyMember.getTeam());
}
로그 출력 결과는 아래와 같습니다.
프록시 객체를 사용해서 Team 엔티티를 조회해오는 것을 확인할 수 있습니다.
위의 상황을 그림으로 표현하면 다음과 같습니다.
이 그림의 순서는 다음과 같습니다.
1. 프록시 객체의 getTeam() 메소드를 호출합니다.
2. 실제 엔티티가 생성되어 있지 않기 때문에 영속성 컨텍스트에 실제 엔티티 생성을 요청함으로 초기화 합니다.
3. 영속성 컨텍스트가 데이터베이스를 조회해서 실제 엔티티 객체를 생성합니다.
4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버 변수에 보관합니다.
5. 프록시 객체는 실제 엔티티 객체의 getTeam()을 호출해서 결과를 반환합니다.
⚠️ 엔티티를 프록시로 조회하는 경우 식별자 값을 파라미터로 전달하기 때문에, 프록시 객체에서 식별자 값을 조회하는 getId()와 같은 메서드를 호출해도 프록시를 초기화하지 않습니다.
정리
- JPA의 프록시 객체는 실제 엔티티를 감싸고 있으며, 연관된 데이터를 사용할 때까지 데이터베이스 조회를 지연시킵니다.
- 프록시 객체의 메서드를 호출하면 Hibernate가 영속성 컨텍스트를 통해 실제 엔티티를 조회하고 초기화합니다.
- 프록시 객체를 초기화해도 실제 엔티티로 변환되지 않고, 단순시 실제 엔티티에 대한 접근을 가능하게 할 뿐입니다.
- 프록시 객체에서 식별자 값을 조회하는 경우 프록시가 초기화 되지 않습니다.
'JPA' 카테고리의 다른 글
OSIV (0) | 2025.02.21 |
---|---|
영속성 전이 & 고아 객체 (0) | 2025.02.19 |
N+1 문제 해결 전략 @BatchSize편 (0) | 2025.02.10 |
JPA 비관적 락 (0) | 2025.02.07 |
JPA 낙관적 락 (0) | 2025.02.06 |