프록시

객체는 객체 그래프로 연관된 객체들을 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다. JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회 할 수 있다.
하지만 자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회하는 것이 효과적이다. JPA는 지연로딩 즉시로딩을 모두 지원한다.

프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것이 아니다. 예를 들어 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비지니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.

private static void printUserAndTeam(EntityManager entityManager) {

  Member member = entityManager.find(Member.class, 3L);
  Team team = member.getTeam();
  System.out.println("회원명 : " + member.getName());
  System.out.println("소속팀 : " + team.getName());
}

위의 코드는 회원과 팀 모두 조회하는 비지니스 로직이다. 회원 엔티티를 찾아서 회원은 정보는 물론이고 연관된 팀 엔티티도 조회한다.

private static void printUser(EntityManager entityManager) {
  Member member = entityManager.find(Member.class, 3L);
  System.out.println("회원명 : " + member.getName());
}

위의 코드는 회원 정보만 조회하는 비지니스 로직이다. 회원 엔티티만 찾고 팀 정보는 조회 하지 않는다.
만약 위의 코드중 회원 엔티티만 조회 하고 팀 엔티티는 조회 하지 않는다면 printUserAndTeam 메소드는 비효율적이다.
이 문제를 해결하고자 JPA는 엔티티를 사용할때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이 방법을 지연로딩이라 부른다.

참고) JPA 표준 명세는 지연로딩의 구현 방법을 JPA 구현체에 위임했다. 그래서 어떤 구현체를 쓰는 것에 따라서 다를 수도 있다.
일반적으로 하이버네이트를 많이 쓰기 때문에 하이버네이트를 구현체로 생각하면 되겠다.

  • 프록시 특징
    프록시 클래스는 실제 클래스를 상속받아서 만들어지므로 실제 클래스와 겉 모양이 같다. 따라서 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면된다.
  • 프록시 위임
    프록시 객체는 실제 객체에 대한 참조를 보관하고 있다. 그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
  • 프록시 객체의 초기화
    프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
private static void proxy(EntityManager entityManager) {
  Member member = entityManager.getReference(Member.class, 3L);
  System.out.println(member.getName());
}

public class MemberProxy extends Member{
  Member target = null;
  public String getName(){
    if(target == null){

      // 초기화 요청
      // DB 조회
      // 실제 엔티티 생성 및 참조 보관
      this.target = ...;
    }
    return target.getName();
  }
}

위는 프록시 클래스의 예상 코드이다.

프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 변수에 보관한다.
프록시 객체는 실제 엔티티 객체의 getName() 을 호출하여 결과를 반환한다.

만약 영속 상태가 끝나고 준 영속 상태에서 초기화를 시도 하면 에러가 발생한다. 하이버 네이트 기준으로 LazyInitializationException
JPA 표준 명세에는 준영속 상태에서 엔티티를 초기화 할 때 어떤 일이 발생할지 표준 명세에는 정의되어 있지 않다.

프록시와 식별자

엔티티를 프록시로 조회할 때 식별자 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.

Team team = entityManager.getReference(Team.class, "team1");
System.out.println(team.getId()); //초기화되지 않음

프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화하지 않는다. 단 엔티티 접근방식을 프로퍼티로 설정한 경우에만 그렇다.
프록시는 다음 코드처럼 연관관계를 설정 할 때 유용하게 사용된다.

Member member = entityManager.find(Member.class, 3L);
Team team = entityManager.getReference(Team.class, "team1"); //sql을 실행하지 않음
member.setTeam(team);

연관관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있다. 참고로 연관관계를 설정할 때는 엔티티 접근 방식을 필드로 설정해도 초기화 하지 않는다.

private static void proxyInit(EntityManager entityManager){
  entityManager.flush();
  entityManager.clear();
  Member findMember = entityManager.getReference(Member.class, 3L);
  System.out.println("프록시 초기화 전");
  System.out.println(entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(findMember));
  System.out.println("findMember = " + findMember.getClass());
  System.out.println(findMember.getName()); //프록시 초기화
  System.out.println("프록시 초기화 후");
  System.out.println(entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(findMember));
}

프록시 인스턴스 초기화 여부를 확인 할 수 있다. entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(Object o) 또는 entityManagerFactory.getPersistenceUnitUtil().isLoaded(Object o) 를 사용 하면된다.
프록시 초기화전에는 false가 나오지만 프록시 초기화 후에는 true가 나온다.
프록시를 조회한건지 아닌건지는 클래스명을 직접 호출해보면 된다.
프록시를 강제로 초기화 할 수 있는데 하이버네이트의 initalize() 메소드를 사용하면 프록시를 강제로 초기화 할 수 있다.
JPA 표준에는 프록시를 강제로 초기화 할 수 있는 메소드가 없다. 강제로 초기화 하려면 member.getName() 처럼 직접 호출해야만 한다.
JPA 표준은 초기화 여부만 확인 가능하다.

출처 : 자바 ORM 표준 JPA 프로그래밍 (김영한)