[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개의 댓글

관련 채용 정보