[JPA] 프록시( Proxy )
위 사진은 영속성 컨텍스트가 동작하는 과정이다.
엔티티 객체를 생성하려면 SELECT문을 DB에 실행해야 한다. APP과 DB는 서로 다른 영역으로 I/O가 발생한다. 잦은 I/O는 성능저하의 원인이 되므로 성능 최적화를 위해 필요한 엔티티 객체만 생성해야 한다.
Member 엔티티를 조회했다고 가정해보자.
Member 엔티티는 Team엔티티와 연관관계에 있다. Member 엔티티 객체가 생성될 때 연관된 Team 엔티티도 만들어져야 한다. 그러나 연관된 모든 엔티티를 객체로 만들면 많은 I/O가 발생한다. 그래서 JPA(하이버네이트)는 성능 최적화를 위해, 연관된 객체는 가짜 객체(프록시,Proxy)로 만들고 실제 엔티티 객체 생성은 지연시키는 전략을 구사한다. 이를 지연 로딩(Lazy Loading)이라 부른다. ( 즉시 로딩 전략도 구사한다. )
프록시는 target 참조변수를 가진다.
프록시가 생성될 때 target은 초기화되어 있지 않다. target은 실제 엔티티 객체가 생성되면 초기화 된다. 프록시의 getter 메소드가 호출되면 실제 데이터가 필요하다. 그러므로 프록시는 영속성 컨텍스트에 요청하고 실제 엔티티 객체는 생성된다. 이것이 JPA(하이버네이트)에서 프록시(Proxy)가 사용되는 원리이다.
getReference()
getReference()는 엔티티매니저로 프록시를 생성하는 메소드이다.
Order order = new Order();
User user = entityManager.getReference(User.class, userId);
order.setUser(user);
위 코드에서 필요한 엔티티는 Order이다. User는 쓰이지 않지만 Order의 참조관계를 세팅할 목적으로 생성되어야 한다. 굳이 DB에 SELECT문을 실행하여 I/O를 만들 필요없이, 엔티티매니저로 영속성 컨텍스트에 요청하여 프록시 객체를 만들면 된다. 그러다가 User의 데이터가 필요할 때 실제 엔티티를 로딩하면 된다.
하나의 세션 안에서 생성된 프록시는 없어지지 않는다. 실제 엔티티 객체가 생성되어도 target이 초기화 될뿐, 엔티티의 객체는 프록시 객체를 가리킨다.
Student 클래스
@Entity
@Data
public class Student {
@Id @GeneratedValue
@Column( name = "STUDENT_ID")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩 전략
ClassRoom classRoom;
}
Main 클래스
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("h2");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction tx = entityManager.getTransaction();
tx.begin();
try {
//ClassRoom 객체 생성
ClassRoom classRoom = new ClassRoom();
classRoom.setName("푸른솔");
//Student 객체 생성
Student student = new Student();
student.setName("이지훈");
student.setClassRoom(classRoom);
//영속화
entityManager.persist(student);
entityManager.persist(classRoom);
entityManager.flush(); //INSERT문 수행
entityManager.clear(); //영속성컨텍스트 초기화
Student findStudent = entityManager.find(Student.class,student.getId()); // STUDENT 엔티티 객체 생성
// 프록시 객체 확인
System.out.println("before CLASS : " + findStudent.getClassRoom().getClass()); // CLASSROOM 프록시 객체 확인
findStudent.getClassRoom().getName(); // 실제 ClassRoom 엔티티 객체 생성
System.out.println("after CLASS : " + findStudent.getClassRoom().getClass()); // CLASSROOM 프록시 객체 확인
tx.commit();
}catch (Exception e) {
e.printStackTrace();
tx.rollback();
}finally {
entityManager.close();
}
}
}
Student 엔티티는 ClassRoom 엔티티와 연관되어 있다. Stundent 엔티티는 find 명령어로 실제 엔티티객체가 생성되었지만 ClassRoom은 지연로딩전략에 의해 프록시객체로 생성된다. 그리고 프록시객체의 getter 메소드를 호출하여 실제 엔티티 객체를 생성했다.
로그와 같이, ClassRoom 엔티티 객체가 실제로 생성되었음에도 Student 엔티티가 참조하고 있는 객체는 프록시 객체이다. 이는 정합성 때문이다. 데이터는 정확하고 일관되게 유지하는 것이 좋다. 상황에 따라 데이터가 바뀌는 것 만큼 혼란스러운 일이 없다. 프록시 객체의 target 참조변수로 실제 엔티티에 접근하는 방식으로 구현된다.
주의할 점
프록시는 영속성컨텍스트의 도움을 받아 초기화되므로 영속성컨텍스트가 살아 있어야 한다. 프록시가 준영속(detach)되거나 엔티티매니저가 종료(close)되거나 초기화(clear)되면 프록시는 영속성컨텍스트의 도움을 받을 수 없게 된다. 영속성 컨텍스트가 없는데 프록시의 getter 메소드를 호출하여 초기화를 시도하면 에러를 뱉어내게 된다. (org.hibernate.LazyInitializationException)
실제 엔티티 객체는 영속성 컨텍스트가 없어도 독립된 객체로 사용 가능하다. 그러나 개발자는 엔티티가 실제 엔티티인지 프록시 객체인지 구분하지 못한다. 개발자는 엔티티 참조변수에 무엇이 들었는지 로그를 찍지 않는 이상 까볼수 없기 때문이다. 그래서 영속성컨텍스트가 사라졌음에도 프록시인 줄 모르고 엔티티를 사용하려다가 오류가 발생하는 경우가 많다.
단, 한번 초기화된 프록시는 언제든 사용이 가능하다. 그래서 초기화 관리가 중요하다.
1) PersistenceUnitUil.isLoaded(Object entity) :실제 엔티티 객체가 로드되었는지 확인할 수 있다.
2) org.hibernate.Hibernate.initialize(entity) : 영속성컨텍스트가 살아있을때 강제로 초기화한다. 이로써 이후 오류 발생 가능성을 없앨 수 있다.
3) entity.getClass().getName() : 엔티티 객체의 이름을 확인하여 프록시인지 확인 가능하다.
참고자료