JPA - Proxy 와 delegate

정성윤·2023년 7월 21일

JPA

목록 보기
1/2

JPA 스터디를 진행하며 Proxy에 대한 강의를 보며 내용을 정리하고 있다보니 옛날 생각이 많이 났다.
이런 얘기를 하면 또또 틀딱거릴거 같은데 눈물없이 들을 수 없는 얘기일 것이다.아마도
이부분은 중요하지 않으니 Proxy에 대해서 먼저 설명해보자.

Proxy

하이버네이트가 내부적으로 실제 클래스를 상속 받아 만드는 가짜 클래스
변수나 메서드 이름 등이 똑같지만 값은 가지고 있지 않은 껍데기만 있는 객체
실제 클래스가 생성되었을 때 매핑되기 위해 참조값을 가지고 있다.

  • 사용하는 입장에서는 똑같이 생겨서 이 객체가 프록시인지 아닌지 알 수 없음
    -> 진짜 객체랑 프록시랑 구분안해서 문제 생기는거 아닐까? 이론상으로는 문제가 없다고 함
    -> 이 부분을 나중에 얘기해보자.

프록시 동작 원리

getReference()

  • 프록시를 사용하려면 기존에 객체를 find 메서드로 찾았다면 getReference 메서드로 찾으면 된다.

    em.find() : db 통해서 실제 엔티티 객체 조회
    em.getReference() : db 조회가 아닌 가짜, 프록시 엔티티 객체를 조회

  • 프록시는 애초에 실제하는 값이 아니므로 db 조회를 할 필요가 없으니 로그를 보면 조회 쿼리도 발생하지 않는다.

  • 그림과 소스를 함께보면서 순서를 이해해 보자.
  1. 사용자의 요청으로 프록시 객체가 생성
  2. 프록시 객체는 껍데기일 뿐이므로, 사용자가 getName()과 같은 실제 데이터값을 요청하게 된다면
    영속성 컨텍스트에게 초기화 요청을 한다.
  3. 영속성 컨텍스트는 그 시점에 DB를 조회하여 실제 엔티티를 생성
  4. 프록시는 실제 엔티티 객체의 Name값을 참조하여 사용자에게 응답
   public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Member member1 = new Member();
            member1.setName("adsgsda");
            em.persist(member1);

            em.flush();;
            em.clear();

            Member findMember = em.getReference(Member.class, member1.getId());
            System.out.println("findid = " + findMember.getId());
            System.out.println("findNm = " + findMember.getName());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        }
        finally {
            em.close();
        }
        emf.close();
    }
  1. Member 라는 엔티티에 우선 데이터를 생성시킨다.
    -> H2라 데이터가 없어서 바로 flush 후 clear로 영속성컨텍스트를 깨끗하게 해놓은 상태
  2. getReference로 프록시 객체 선언
  3. 하이버네이트는 Member를 상속받은 프록시 객체를 생성시킴

    -> 물론 실제 Member를 참조할 수 있는 주소값, 즉 target값도 포함
    -> 중요한건 여기까지의 과정에서 주소값은 아직 없음

언제 영속성 컨텍스트에 초기화 요청을 하는것일까?

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;


            System.out.println("findid = " + findMember.getId());
            System.out.println("findNm = " + findMember.getName());

- PK값인 id를 JPA가 할당하여 생성하므로, 하이버네이트가 해당 long값을 알고 있다
스터디원이 링크를 보내와 정확하게 다시 확인해주셨다.
내용이 깊으니 정리를 대충 하자면

프록시 초기화 과정을 담당하는 ByteBuddyInterceptor 라는 클래스가 있음
정확하게는 이 놈의 상위 추상 클래스 AbstractLazyInitializer 가 담당함

@Override
public final Serializable getIdentifier() {
    if (isUninitialized() && isInitializeProxyWhenAccessingIdentifier() ) {
        initialize();
    }
    return id;
}

해당 조건문에서 보통 false 되므로 id가 리턴되는 것임
저 조건은 hibernate.jpa.compliance.proxy 옵션이 기본값이 false 라서
그런데 명심해야할 것은 자바 빈 규약에 맞는 호출을 해야지만 해당 구문을 탄다는 것!
get+필드명이 아니면 안된다와 다른 조건이 있었는데, 더이상 알 필요는 지금 없을 것 같다..

중요한 것은 ID값을 알고 있으므로 DB에 조회할 이유가 없다는 것!
findMember.getId()까지는 초기화요청을 하지 않는다!

  • Name값은 알 수 없으므로 DB를 조회하여 로그에 조회쿼리를 확인할 수 있다.
  1. 영속성컨텍스트가 실제 Member 엔티티를 생성하여 프록시가 그 값을 참조하여 리턴

프록시의 특징

1. 프록시는 처음 선언시 딱 한번 초기화

  • 초기화 요청시 프록시 객체가 실제 엔티티가 되는 것이 아님을 꼭 생각하자.
    -> 이 프록시 객체를 통해 생성된 엔티티 객체로 접근이 가능하다는 것
    -> 엔티티 상속을 받긴 하므로, 타입체크를 해야한다면 instance of 를 이용하자.
private static void logic(Member m1, Member m2) {
        System.out.println("m1 == m2 : " + (m1 instanceof  Member));
        System.out.println("m2 == m1 : " + (m2 instanceof  Member));
    }

2. 영속성 컨텍스트에 호출하려는 Id 값이 존재한다면, 프록시로 호출 불가

JPA는 영속성 컨텍스트의 동일성을 보장해야 한다.
키값이 같은 동일한 객체가 두개가 존재하면 안되므로

  • 즉 find로 id가 1번인 엔티티를 1차 캐시에 담았는데, getReference로 1번을 호출한다해도 그 객체는
    프록시 객체가 아닌 엔티티 객체로 리턴된다는 것이다.
  • 그렇다는 말은, getReference로 1번을 호출하고 영속성 컨텍스트에 초기화 요청을 하여 엔티티가 생성되면
    find로 id가 1번인 엔티티를 선언해도 프록시 객체가 리턴된다는 것!

3. 영속성 컨텍스트의 도움을 받을 수 없을 때 프록시 초기화하면 오류발생

  • 초기화라는것은 결국에 영속성 컨텍스트가 DB를 조회해서 엔티티를 생성시키는 과정
  • 그런데 영속성 컨텍스트가 준영속 상태(프록시가 관리대상에서 빠진다거나), 또는 닫히거나, 초기화되면
    org.hibernate.LazyInitializationException 예외를 발생시킨다.

    그래서 코드를 트랜잭션과 영속성 컨택스트는 시작과 끝을 맞춰서 짜는게 일반적이라고 한다.

프록시 상태 확인

  • 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity) 
//정확하게는 팩토리에서 써야함 
emf.getPersistenceUnitUtil().isLoaded(entity)
  • 프록시 클래스 확인 방법
    entity.getClass()
    출력하면 HibernateProxy 어쩌구 라고 나옴
  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);
    JPA 표준은 강제 초기화가 없어서 그냥 .getName() 으로 한번 불러서 쿼리 쓰게 만든다고 함

이렇게 프록시에 대해서 알아보았다.
프록시 서버와는 또 비슷비슷하면서 다른게 역시 어려운게 참 많은거 같다.

그래서 왜 delegate?

강의를 쭉 듣다보니 이거 delegate네 라는 생각이 그냥 들었다.
그리고 자료에서도 delegate라는 단어가 나와버리면서 추억에 젖어들고 말았던 것.
근본은 잊지 말자는 생각에 리마인드의 개념으로 작성해보고자 한다.

왜 눈물이 나냐면 C#은 스레드와 같이 동적처리를 시작하는 변수이기 때문에
여기서부터 사람들이 멘탈이 터지고 힘들어하기 때문이다.
정말 아무것도 모를시절 회사에서 혼나면서 동적처리를 어떻게 배웠을지 상상해보라..

C#의 proxy, delegate

  1. 메서드를 간접 참조
  2. 런타임에 동적 기능
  • 델리게이트 기본 형식과 예제
public delegate [반환형식] [이름] (매개변수)

namespace ConsoleApplication
{
    delegate int FuncDelegate(int a, int b);

    class Program
    {
        static int Plus(int a, int b)
        {
            return a + b;
        }
        
        static int Minus(int a, int b)
        {
            return a - b;
        }

        static void Main(string[] args)
        {
            FuncDelegate plusDelegate = Plus;
            FuncDelegate minusDelegate = Minus;

            Console.WriteLine(plusDelegate(5, 10));
            Console.WriteLine(minusDelegate(20, 10));
        }
    }
}

출력)
15
10

  • Plus()와 Minus() 함수를 delegate 'FuncDelegate'에 연결하여, 각각 연결된 함수가 호출되고 있다.
  • 선언한 delegate의 반환/매개변수는 참조하는 함수(Plus/Minus)와 일치한다.

왜 쓰는지 정확하게 모를 수 있다. 조금 더 길게 한번 보자.

using System;

namespace ConsoleApp1
{
    delegate void FuncDelegate(string str);

    class Program
    {
        static void Func1(string str)
        {
            Console.WriteLine("Helo1 : " + str);
        }
        static void Func2(string str)
        {
            Console.WriteLine("Helo2 : " + str);
        }
        static void Func3(string str)
        {
            Console.WriteLine("Helo3 : " + str);
        }
        static void Func4(string str)
        {
            Console.WriteLine("Helo4 : " + str);
        }
        static void Main(string[] args)
        {
            FuncDelegate plusDelegate = null;
            plusDelegate += Func1;
            plusDelegate += Func2;
            plusDelegate += Func3;
            plusDelegate += Func4;

            plusDelegate("Text");
        }
    }
}

출력 )
Hello1 : Text
Hello2 : Text
Hello3 : Text
Hello4 : Text

  • 하나의 delegate 함수 'FuncDelegate'에 여러개의 함수(Func1~4)를 등록
  • 한번 호출로 4개를 전부 사용 가능
  • '+=' 연산자를 통해 함수를 등록, '-=' 연산자를 통해 제거할 수 있음
  • 여러개의 함수를 등록하고 호출할 수 있는 이것을 '델리게이트 체인(Delegate Chain)' 이라 한다.

프록시랑 뭐가 비슷한데?

  • 프록시는 초기화 이후 실제 엔티티의 값을 참조하여 리턴시켜준다.
  • 델리게이트 또한 선언 이후 동일한 형식을 가진 메서드를 연결하여 리턴시킬 수 있다.
  • 프록시는 초기화 이후 실제 엔티티의 참조값을 계속 불러와서 자기가 일을 하는, 이른바 대리업주 역할을 함
  • 델리게이트 또한 체인으로 등록된 함수를 자기 일마냥 계속 하고 있음

이른바 C의 함수 포인터와도 비슷한 애들이라 이말인 것이다.
왜 델리게이트가 중요하냐면, C#에서는 이놈을 이해해야만 이벤트라는 거대한 산을 만날 수 있다.
이벤트 핸들러가 델리게이트이고, 심지어 스레드의 시작을 알리는 TheadStart라는 함수도 델리게이트 였다.
아닌가?
무튼 동적처리 하는 놈들은 다 위험한 친구들이니 이제부터 더 난이도가 높아질거 같다.

출처 : https://m.blog.naver.com/dontcryme/30093395886
https://huiyu.tistory.com/entry/C-%EA%B8%B0%EC%B4%88-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%99%80-%EB%8D%B8%EB%A6%AC%EA%B2%8C%EC%9D%B4%ED%8A%B8-Event-Delegate
https://www.csharpstudy.com/CSharp/CSharp-delegate-concept.aspx

profile
이제 운동을 좋아해야만 하는데

0개의 댓글