[JPA] 프록시란?
Proxy란 JPA에서 지연로딩을 할때 굉장히 중요한 역할을 하는 녀석이라고 한다. 이 프록시가 어떤것인지 알아보도록 하자
JPA Proxy란
JPA에서 프록시는 연관된 객체들을 데이터 베이스에서 조회하기 위해 사용한다. 프록시를 사용하면 연관된 객체들을 처음부터 데이트베이스에서 조회하는것이 아닌 실제 사용하는 시점에서 데이터 베이스에서 조회를 하는것이 가능하다
하지만 자주 함께 사용되는 객체들은 조인을 사용하여 함께 조회하는것이 더 효과적이다.
프록시 기초
- 지연 로딩을 이해하려면, 프록시의 개념에 대해 명확하게 이해해야한다.
em.find()
em.find() 는 DB를 통해서 실제 엔티티 객체를 조회하는 메서드이다,
em.getReferencce()
em.getReference() 는 DB의 조회를 미루는 가짜(프록시) 엔티티를 조회하는 메서드이다.
EnttiyManager의 getReference() 메서드를 호출하면 엔티티를 바로 조회해 오지 않고, 실제 사용하는 시점에 조회를 해올수 있다. 이러한 지연로딩을 지원하기 위해 프록시 객체를 사용하는데, 반환되는 프록시 객체의 모습은 아래 같이 구현된다.
class MemberProxy extends Member{
Member target = null;
public String getName(){
if(target == null){
// DB 조회
// 실제 엔티티 생성 및 참조 보관
this.target = // ...
}
return this.target.getName();
}
}
// 사용
@Test
public void getReferneceTest(){
Member member = em.getReference(Member.class, 1);
member.getName();
}
위의 코드처럼 프록시 객체는 상속을 사용하여 구현이 된다. 내부에선 역속성 컨텍스트에 의해 데이터베이스를 조회해 실제 엔티티를 생성하는 __프록시 객체의 초기화__라고 한다. 흐름은 아래와 같다. (영속성 컨텍스트는 비어있다가정한다.)
- getReference()를 호출하면 프록시 객체를 생성한 뒤 영속성 컨텍스트 (1차 캐시)에 저장한다.
- 실제 데이터를 얻기 위해 getName을 호출한다.
- 프록시 객체는 영속성 컨텍스트에 실제 엔티티 생성을 요청한다. (초기화)
- 영속성 컨텍스트는 데이터 베이스를 조회해서 실제 엔티티 객체를 생성하고, 해당 객체의 참조를 target 변수에 보관한다.
- 프록시 객체는 target 변수에 저장된 실제 앤티티 객체의 getName()을 호출하여 결과를 반환한다.
프록시 객체는 다음과 같은 특징을 가지고 있다.
- 프록시 객체의 초기화는 딱 한번만 실행된다. target 객체에 저장되면 그것을 계속 사용하기 떄문,
- 원본 엔티티를 상속 받은 객체이므로 타입 체크에 주의해야 한다.
- em.getReference() 실행 시 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 (식별자로 조회), 프록시 객체 대신 실제 엔티티를 반환한다.
Member member1 = em.getReference(Member.class, 1);
Member member2 = em.find(Member.class, 1);
System.out.println(member1 = member2); // true
처음 호출 될 때 식별자를 이요하여 1차 캐시에 저장하고, 초기화 되면 해당 프록시 객체내의 target 변수에 값이 저장되게 된다. 이후에 em.find()로 엔티티를 조회해도 이미 프록시 객체가 저장되어 있기 때문에 해당 객체가 그대로 반환된다.
프록시와 식별자
프록시 객체는 target 변수만 가지고 있는 것이 아니라, 전달받은 식별자 값도 같이 저장된다. 그러므로 아래와 같이 식별자 값만 조회할 경우 직접적인 데이트 베이스의 조회가 일어나지는 않느다.
Member member = em.getReference(Member.class, 1);
member.getId(); // SQL 실행 X
이러한 특징을 이용하여, 연관관계를 설정할 때 유용하게 사용할 수 있다.
Member member = new Member();
member.setName("Han");
member.setAget(27);
// team setting
Team team = em.getReference(Team.class, 1);
member.setTeam(team);
em.persist(member);
데이터베이스에서 연관관계를 설정할때 외래키로 해당 데이터베이스의 식별자 밖에 사용하지 않는다. 즉, member 를 persist할 때 team의 id 만 필요할 것이고, 실제로도 그렇게 처리될 것이다. 이럴 경우 team을 전체 조회해오는 find 보다는 getReference()를 사용해서 데이터베이스 접근횟수를 줄일 수 있다. 하지만 이런경우 외래키 제약조건을 안걸면 db레벨에서 오류가 발생하지 않으므로 위험할 수 있다.
프로시 확인
JPA에서 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용하면 프록시 객체의 초기화 여부를 확인할 수 있다. 아직 초기화 되지 않은 엔티티의 경우 false 를 반환한다.
즉시로딩, 지연로딩에서의 프록시
JPA에서는 연관된 엔티티를 조회해올 떄도 프록시 객체를 사용하여 지연로딩을 할 수 있다. 지연로딩 여부는 연관관계를 맺는 어노테이션(@ManyToOne, @OneToMany) 의 속성(fetch)로 제공하여 상황에 따라 개발자가 선택해서 사용할 수 있게 해준다.
즉시로딩
fetch 속성을 FetchType.EAGER 로 주면된다.
@Entity
class Member{
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩으로 설정
@JoinColumn(name = "team_id")
private Team team;
}
Member member = em.find(Member.class, 1);
Team team = member.getTeam(); // 프록시 객체
team.getName(); // 이때 조회됨!
em.find(Member.class, 1) 을 호출하면 Member만 조회를 하고 team 멤버변수에는 프록시 객체를 넣어둔다. 그리고 아래 실제 사용되는 부분에서 데이터가 조회된다, (동작 방식은 em.getReference()와 동일하다) 사용 시점에 조회해오므로 쿼리는 당연히 따로따로 날라간다,
컬랙션 래퍼
하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 해당 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
이를 컬렉션 래퍼라고 하고 org.hibernate.collection.internal.PersistentBag 클래스이다.
클래스가 컬렉션 레벨에서 프록시 객채의 역할을 해준다. 이 클래스를 통해 지연로딩을 달성할 수 있다.
컬렉션의 실제 데이터를 조회할때 데이터 베이스를 조회해서 초기화 한다.
Comments