Programming-[Backend]/JPA

[JPA기본] 9. 프록시 객체

컴퓨터 탐험가 찰리 2021. 10. 15. 05:30
728x90
반응형

 

1. 프록시 기초


프록시는 JPA의 실제 엔티티를 필요할때만 꺼내쓸 수 있도록 하는 가짜 객체이다. 실제 클래스를 상속받아서 만들어지며 겉모양이 같기 때문에 이론적으로는 사용자가 Proxy 객체를 그대로 써도 문제가 없다. 프록시 객체에는 아래와 같이 상속받은 엔티티 타입의 target이라는 필드값이 있다. 실제 데이터가 필요하면 이 target을 통해 id, name 등 원래 entity의 속성값을 조회한다. 아래 예제를 살펴보자.


아래와 같이 1:N의 관계로 연관지어진 Member와 Team 객체 관계에서 member 객체를 불러와서 SQL문을 확인해보자.

src/main/java/com/example/jpamain/JpaMainApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tx.begin();
 
    try {
 
      Member member = new Member();
      member.setName("사용자");
 
      em.persist(member);
 
      em.flush();
      em.clear();
 
      Member findMember = em.find(Member.class, member.getId());
 
      tx.commit();
cs

 

 

JPA는 기본적으로 연관관계를 갖는 모든 엔티티를 조인해온다.

member 객체만 불러왔는데, 연관 관계를 갖는 Team 클래스까지 조인해오는 것을 확인할 수 있다. 이렇게 되면 실제로는 member.getName으로 특정 member의 이름값만 얻고 싶은데, 해당 상황에서는 불필요한 Team 엔티티까지 불러오게 되어 효율적이지 못하게 된다.

em.find() VS em.getReference()

그래서 em.find() 대신 em.getReference()라는 메서드를 사용하면 실제 필요한 순간까지 연관관계를 갖는 엔티티를 Proxy 형태로만 불러온다. 위 코드에서 em.find를 em.getReference로 변경하면, 실제 값이 필요한 순간까지 객체의 호출을 미루게 된다. 다시 말해 em.getReference 호출문 아랫 부분에 findMember.getName() 등과 같이 특정 값을 요청하면 그제서야 쿼리문을 실행하게 된다.

em.find와 em.getReference를 각각 실행했을 때, getClasss를 통해 출력되는 클래스 타입이 다른 것을 확인할 수 있다. 프록시의 경우 지정한 엔티티가 아니라 HibernateProxy의 형태로 출력된다.

 


 

2. 프록시의 특징

 

프록시의 초기화


프록시 객체는 실제 값이 필요할 때, DB에 값을 요청하게 된다. 이때 앞서 언급한 바와 같이 target 필드에 실제 엔티티의 값들이 주입된다. 그리고 불러온 엔티티에 대한 정보가 영속성 컨텍스트에 저장된다. 프록시 객체는 처음 사용할 때 한 번만 초기화된다. 이후에는 영속성 엔티티에 있는 값을 계속 가져다 쓰게된다.

프록시는 실제 엔티티로 변하는 것이 아니다

프록시 객체를 초기화하면, 프록시 객체를 통해서 실제 엔티티에 접근이 가능하게 된다. 그러나 프록시 객체가 엔티티로 변하는 것이 아니라 실제 엔티티에 계속 접근할 수 있도록 연결되는 것이라고 이해해야한다. 프록시는 유지되고, 프록시 내부의 target에만 값이 채워진다.

실제 조회 뒤에도 findMember 클래스가 프록시 객체의 형태로 출력되는 것을 확인할 수 있다.

 

JPA 엔티티 비교시, ==이 아니라 instanceof을 쓰자

프록시 객체는 실제 엔티티를 상속받는 객체이다. 따라서 만약 실제 엔티티와 프록시 객체가 혼재된 상태에서 == 비교를 하면 겉보기에는 같은 엔티티인데, 실제 엔티티와 프록시 객체를 ==비교하여 원하지 않는 결과가 나올 수도 있다. 따라서 엔티티 비교 시 instanceof을 사용해야한다. 아래 예시를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tx.begin();
 
    try {
 
      Member member1 = new Member();
      member1.setName("사용자");
 
      Member member2 = new Member();
      member2.setName("사용자2");
 
      em.persist(member1);
      em.persist(member2);
 
      em.flush();
      em.clear();
 
      Member findMember1 = em.find(Member.class, member1.getId());
      Member findMember2 = em.getReference(Member.class, member2.getId());
 
      System.out.println("is same ? " + (findMember1.getClass() == findMember2.getClass()));
 
      tx.commit();
cs

 

em.find로 실제 엔티티를 불러오고, em.getReference로 프록시 객체를 불러와서 == 비교를 해봤다. 같은 Member 클래스이기 때문에 true가 나와야할 것 같지만, 프록시 객체는 실제 Memeber 객체가 아니라 상속받은 객체이기 때문에 false가 결과로 출력된다.

예시 코드에서는 em.find, em.getReference로 다르게 호출한 것을 확인할 수 있지만, 실무에서는 이런 부분이 메서드로 은닉화 되어있거나, 다른 방식으로 엔티티를 조회하여 조회한 엔티티가 실제 엔티티인지, 프록시인지 모르는채로 쓰는 경우가 많기 때문에 아래와 같이 instanceof을 이용해야한다.



JPA는 1개의 트랜잭션에서 같은 id로 조회한 엔티티와 프록시는 ==비교가 가능하도록 1개 타입만 반환한다.

실무에서 잘 맞닥뜨리지는 않는 문제이지만, 중요한 개념이다. 위와 같이 다른 id값을 갖는 member1, member2가 아니라 id값이 완전히 같은 엔티티를 엔티티와 프록시 형태로 호출하면 1개의 트랜잭션 내에서는 완전히 같은 형태의 객체를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tx.begin();
 
    try {
 
      Member member1 = new Member();
      member1.setName("사용자");
 
      em.persist(member1);
 
      em.flush();
      em.clear();
 
      Member findMember = em.find(Member.class, member1.getId());
      System.out.println("findMember1 = " + findMember.getClass());
 
      Member reference = em.getReference(Member.class, member1.getId());
      System.out.println("reference = " + reference.getClass());
 
      System.out.println("result = " + (findMember.getClass() == reference.getClass()));
      tx.commit();
cs

 

em.find 후 em.getReferece로 프록시를 호출하려고 했음에도 실제 엔티티인 Member 타입이 호출된 것을 볼 수 있다. 따라서 == 비교가 성립된다.

재밌는 것은, 반대의 경우에도 ==이 성립한다는 것이다. 위의 경우 이미 엔티티를 불러와서 영속성 컨텍스트에 저장한 상태이므로 굳이 프록시 형태로 불러와서 성능 낭비를 할 필요가 없다는 명분이 있다. 그러나 반대의 경우에는 이러한 이점이 없더라도 JPA에서 == 비교를 보장하기 위해 프록시 형태로만 엔티티를 조회한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tx.begin();
 
    try {
 
      Member member1 = new Member();
      member1.setName("사용자");
 
      em.persist(member1);
 
      em.flush();
      em.clear();
 
      Member findMember = em.getReference(Member.class, member1.getId());
      System.out.println("findMember1 = " + findMember.getClass());
 
      Member reference = em.find(Member.class, member1.getId());
      System.out.println("reference = " + reference.getClass());
 
      System.out.println("result = " + (findMember.getClass() == reference.getClass()));
      tx.commit();
cs

 

준영속 상태일때 프록시를 초기화하면 문제가 발생한다.

(하이버네이트는 org.hibernate.LazyInitializationException 예외 발생)


프록시는 엔티티의 값이 필요할때 초기화가 되며, 이 초기화는 영속성 컨텍스트를 통해서만 이루어진다. 따라서 이 영속성 컨텍스트에서 객체가 detach 되거나, 영속성 컨텍스트 자체가 close가 되어버리면 문제가 발생한다. 하이버네이트에서는 LazyInitializationException 예외가 발생한다.

실무에서 종종 볼 수 있는 문제로, 그 원인이 되는 것이 프록시이며 영속성 컨텍스트에 해당 객체가 영속 객체로 관리가 되지 않거나 영속성 컨텍스트 자체가 문제가 있을 것이라는 추측을 할 수 있어야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tx.begin();
 
    try {
 
      Member member1 = new Member();
      member1.setName("사용자");
 
      em.persist(member1);
 
      em.flush();
      em.clear();
 
      Member refMember = em.getReference(Member.class, member1.getId());
      System.out.println("findMember1 = " + refMember.getClass());
 
      em.detach(refMember);
//      em.close();
      
      refMember.getName();
 
      tx.commit();
 
cs




3. 프록시 확인



프록시 클래스 확인
entity.getClass().getName() 으로 가능하다.

프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity) 로 가능하다. 프록시의 특정 필드값을 불러오는 형태로 초기화를 하지 않고 그대로 초기화가 가능하다. 참고로 JPA 표준은 강제 초기화가 없으며 hibernate에서 제공하는 기능이다.


 

참조

 

1. 인프런_자바 ORM 표준 JPA 프로그래밍 - 기본편_김영한 님 강의

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21735?tab=curriculum

728x90
반응형