원래 즉시로딩과 지연로딩에 관해 정리를 하려고 했으나 관련 자료를 찾다보니 프록시(Proxy)를 먼저 이해해야할 필요성을 느껴 먼저 작성하게 되었다.
JPA에서는 한 객체를 조회할 때 연관되어있는 엔티티를 모두 조회하는 것이 아닌 필요한 연관관계만 조회하는 방식인 지연로딩(LAZY)을 지원한다. JPA의 구현체 하이버네이트(Hibernate)가 프록시 객체를 동해 지연로딩을 구현하고있다.
Proxy
프록시는 동작을 대신해주는 가짜 객체의 개념이다. JPA뿐 아니라 다양한 곳에서 사용되는 개념이다.
하이버네이트에서는 연관된 엔티티의 실제 데이터가 필요할때까지 조회를 미루는 과정에서 연관관계 자리에 프록시 객체를 주입하여 실제 객체가 있는것처럼 동작하게 만드는 방식으로 지연로딩을 구현한다.
프록시는 실제 객체를 상속한 타입을 가지고있기 때문에 실제 객체타입 자리에 들어가도 문제가 없다. 그래서 JPA 엔티티 생성 시 아래와 같은 규칙이 만들어지게 되었다.
- 기본 생성자는 최소 protected 접근 제한자를 가져야 한다.
- 엔티티 클래스는 final로 정의할 수 없다.
기본 생성자가 private이면 프록시 생성 시 super를 호출할 수 없을 것이고, 엔티티를 final로 선언한다면 상속이 불가능하게 된다.
초기화
최조 지연로딩 시점엔 참조값이 없기 때문에 실제 객체의 메서드를 호출할 필요가 있을때 DB를 조회해서 참조값을 채우게된다. 이를 프록시 객체 초기화라고 한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@JoinColumn(name = "team_id")
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
...
}
=======================================
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
public Team(String name) {
this.name = name;
}
...
}
================================
Team team = member.getTeam();
// 이 시점에 프록시 초기화
Member의 연관관계 Team은 지연로딩으로 설정이 되어있어 Member조회 시 team필드 자리에 프록시가 들어가고, Team의 getName 메서드를 호출하게 되면 select 쿼리가 실행되면서 프록시가 초기화된다.
그렇다고해서 프록시가 실제 객체로 바뀌는 것은 아니고 실제 객체를 참조만 하게되는 것이다.
프록시의 초기화는 영속성 컨텍스트의 도움을 받는다. 영속성 컨텍스트의 관리를 받지 못하게 되는
- 준영속 상태의 프록시 초기화의 경우
또는
- 기본값으로 켜져있는 OSIV 옵션이 꺼져있는 상황에서 트랜잭션 바깥에서 프록시를 초기화 하려는 경우 OSIV (Open Session In View - 영속성 컨텍스트를 뷰까지 열어두는 기능. 영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지.)
일때
LazyInitializationException
이 발생하게된다. 따라서 프록시 초기화 시 프록시는 영속상태여야한다.
참고
- https://ict-nroo.tistory.com/131
- https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/