[JPA] 프록시(Proxy)

Done is better than perfect·2021년 11월 7일
1

JPA

목록 보기
11/12
post-thumbnail

JPA에서 객체를 조회하는 두가지 메소드가 있다.
(두 메소드 전부 EntityManager에 정의된 메소드이다.)

1. em.find()

데이터베이스를 통해서 실제 엔티티 객체 조회

2. em.getReference()

데이터베이스 조회를 미루는 가짜 엔티티(프록시) 객체 조회

이 중 getReference() 메소드는 프록시 객체를 조회해오는 메소드인데, 프록시(Proxy)의 사전적 의미는 대리인으로, JPA에서만 사용하는 한정적인 개념이 아니다. 이번 글에서는 JPA에서 사용하는 프록시에 대해 중점적으로 정리해두고자 한다.

프록시특징은 다음과 같다.

- 실제 클래스를 상속 받아서 만들어졌다.
- 실제 클래스와 겉모양이 같다.
- 사용하는 입장에서는 진짜 객체와 프록시 객체를 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 통해 메소드를 호출하면 프록시 객체는 실제 객체의 메소드 호출

위 특성 중 [프록시 객체는 실제 객체의 참조(target)를 보관] 특성에 조금 더 알아보자.
target은 getReference()로 프록시 객체를 가져왔을 때 부터 초기화가 되어있을까?
정답은 아니다. 최초 프록시 객체의 target은 null값을 갖고 있으나, 그 객체 필요한 값을 가져 올 때 초기화가 된다.

🎈예제코드

Member member = new Member();
member.setUsername("member1");
em.persist(member);

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


Member findMember = em.getReference(Member.class, member.getId());

// getClass() 메소드의 결과를 출력해보자
System.out.println("findMember.getClass() = " + findMember.getClass());

// 어느 시점에 초기화가 될까? 예측해보자
System.out.println("findMember.getUsername() = " + findMember.getUsername());

🎉결과

findMember.getClass() = class jpabook.jpashop.domain.Member$HibernateProxy$PIBDFjyQ

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        member0_.USERNAME as USERNAME2_0_0_,
        team1_.TEAM_ID as TEAM_ID1_1_1_,
        team1_.name as name2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
        
findMember.getUsername() = member1

위에서 언급했다시피 getClass() 메소드의 결과를 출력해본 결과 순수 Member 클래스가 아닌, Member$HibernateProxy$PIBDFjyQ 라는 프록시 클래스가 출력된 것을 볼 수 있다.
(이 출력이 나오기 전에 조회 쿼리가 날아가지 않은 점도 주목해봐야한다.)

중요한 것은 이 시점에 해당 프록시 객체의 target은 아직 null 이라는 것이다.

다음 라인을 살펴보자. getUsername() 메소드를 호출하고 있다. (target이 null이라면서 저 메소드는 어떻게 호출하지? 라는 의문을 가질 수 있다. 현재 프록시는 Member를 흉내내는 객체라 Member가 갖고 있는 메소드 역시도 전부 갖고 있다. target은 프록시가 갖고 있는 속성 중 하나라고 생각하자)

실제 객체의 데이터를 요구하는 것이다. JPA는 그 전까지는 Proxy 객체를 초기화 하지 않고 대기할 수 있지만 더 이상 그럴 수 없다. 당연한 결과 아닐까? 이젠 실제 데이터가 필요하니까! 그래서 이 시점에 실제 select 문이 실행되고, getUsername()의 결과 member1을 출력한다.

이와 관련하여 프록시의 추가적인 특징은 다음과 같다.

- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생

위 언급한 특징은 실제 예제코드를 하나씩 따라해보며 확인하는 것을 추천한다!


처음에 언급했듯 **프록시**는 JPA에 한정적인 개념이 아니다. 여러 프로그래밍 언어나 프레임워크 등에서 사용중인 일종의 디자인패턴이자 프로그래밍 기법정도로 볼 수 있다. 그렇다면

JPA에서 프록시 패턴이 왜 사용됐을까? 무엇을 위해 사용했을까?

JPA에서는 엔티티를 조회할 때 즉시로딩지연로딩이라는 중요한 개념이 있다. 각각의 개념이 무엇인지 이해하는 것은 JPA를 사용하는데 있어서 중요한 부분인데, 이 중요한 개념을 JPA는 내부적으로 프록시를 이용하여 구현하였다. 그러므로 이를 정확히 이해하기 위해서 먼저 사전작업으로 프록시에 대해 알아보았다. 다음 글에서는 프록시에 대한 개념을 바탕으로 즉시로딩지연로딩에 대해 알아보겠다!

0개의 댓글