새소식

xub2의 개발 새발 🐾
💽 Database/JPA

[JPA] getReference() & Proxy : 매우 중요 !!!

2025. 9. 17. 14:45

  • -

만약 Member 엔티티를 조회할 때, 필드로 가지고 있는 Team도 같이 조회해야할까?

뭔소리인지 코드로 한번 보자

@Test
public void 회원_팀_동시조회() throws Exception {
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try{
        Team team = new Team();
        team.setName("알나스르");
        em.persist(team);

        Member member = new Member();
        member.setUsername("호날두");
        member.setTeam(team);
        em.persist(member);

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

        Member findMember = em.find(Member.class, member.getId()); // 여기서 회원을 조회할 때 팀까지 조회할 수 있으면 좋긴 함
        //printMemberAndTeam(findMember); // 원래 사용하던 메서드는 멤버 + 팀을 다 가져와야하는데 다음 메서드로 교체
        printMember(findMember); // 이 메서드는 근데 굳이 Team 을 안가져와도 된다.

        // 어느 경우에는 둘다 가져오는게 이득이고, 어느 경우에는 하나만 가져오는게 이득인지는 비즈니스 상황에 따라 다르다.
        // 그렇다면 어떤 방법을 택할 수 있을까



        tx.commit();

    } catch(Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
}

private static void printMember(Member findMember) {
    System.out.println("findMember = " + findMember.getUsername());
}

private static void printMemberAndTeam(Member member) {
    String username = member.getUsername();
    System.out.println("username = " + username);

    String teamName = member.getTeam().getName();
    System.out.println("teamName = " + teamName);
}

만약 어떤 상황에서는 두 엔티티를 다 가져오는게 이득일 수도 있고, 어떤 상황에서는 하나만 가져오는게 이득일 수 있다.

그럼 상황에 따라 적적하게 선택하는 방법이 없을까? -> JPA에서는 이를 지연로딩 + 프록시 기술을 통해 이러한 상황에 대처할 수 있도록 지원한다.


Proxy란?

원래 JPA에서 em.find()로 데이터를 찾아왔다. find() 메서드는 실제 DB에서 데이터를 가져오는 메서드이다.

JPA는 추가적으로 데이터를 가져오는데, 프록시를 적용한 데이터를 가져올 수 있는 em.getReference() 메서드도 지원하는데, 이는 데이터베이스 조회를 미루는 가짜 엔티티 객체를 가져오는 역할을 한다. -> 즉, DB에 쿼리가 안나가지만 객체가 찾아와진다 (자세히는 프록시 객체가 가져와짐).

//기존에 데이터를 가져오던 방식
Member findMember = em.find(Member.class, member.getId()); // 곧바로 쿼리 나감
System.out.println("findMember = " + findMember.getId());
System.out.println("findMember = " + findMember.getUsername());
            

// 프록시 객체를 가져오는 방식
Member findMember = em.getReference(Member.class, member.getId()); // 여기까진 쿼리 안나감
System.out.println("findMemberId = " + findMember.getId()); // 실제 객체를 사용하는 시점에서야 쿼리가 나간다
System.out.println("findMemberName = " + findMember.getUsername());

기존에 em.find()로 데이터를 찾게 되면, 곧바로 SELECT 쿼리가 나가게 된다.

하지만 em.getReference로 데이터를 찾게 되면, 조회 시점에는 쿼리가 나가지 않고 실제로 getXXX 메서드로 해당 객체를 사용하게 되는 시점에 쿼리가 나가서 데이터를 가져오게 된다.

클래스를 찍어보면 프록시 클래스가 찍힘

 

그래서 작동원리가 뭔가요

먼저 하이버네이트가, getReferece() 메서드를 실행하게 되면 실제 객체가 아닌 Member 객체를 상속받은 빈껍데기 프록시 객체를 주입한다.

더 자세하게 까보면, target 필드(실제 객체 참조값) + 기존 조회 대상 클래스 필드 로 구성되어 있고, PK값 정도만 들고 있는 객체이다.

1. 최초에는 getReference로 조회시 target 필드는 비어있을 것이다.

2. 이후, 해당 프록시 객체에 대해 getName() 메서드 처럼 실제 객체를 사용해야하는 로직이 실행되면, 그때 JPA가 영속성 컨텍스트에 실제 해당 객체를 요청한다(초기화 단계). 

3. 그다음 영속성 컨텍스트에서 실제 쿼리가 나가게 되고 리턴 받은 데이터를 통해 실제 엔티티를 생성한 뒤, 프록시 객체의 target 필드에 생성한 엔티티의 참조값을 삽입하게 된다.

4. 이후 target.getName()을 실행하게 되면 <실제 엔티티 참조값.getName()> 메서드가 실행되어 결과를 리턴받을 수 있게 된다.

5. 추가적으로 이미 영속성 컨텍스트에 해당 객체가 존재하기 때문에 이후 호출하는 메서드들에 대해서는 영속성 컨텍스트에서 처리가 가능하다.


프록시의 특징

1. 위에서 말한 5번 단계처럼, 처음 사용할 때 한번만 초기화 하고 그 이후에는 영속성 컨텍스트에 올라와 있기에 다시 찾아오지 않아도 된다.

2. 프록시 객체는 실제 엔티티로 변화하는 것이 아닌, 프록시 객체를 통해서 실제 엔티티에 접근이 가능해진다.

3. 이게 중요한데, 프록시 객체는 원본 엔티티의 자식 클래스이기 때문에, 타입 체크시 instance of 를 사용해야한다. 반드시 타입 검증은 instanceof를 사용하자.

if(findMember.getClass() == Member.class) // 이렇게 쓰면 안됨

if(findMember instanceof Member) // 이렇게 써야 함

 

4. 영속성 컨텍스트에 이미 찾는 엔티티가 이미 있다면 em.getReference를 실행해도 실제 엔티티가 반환된다.

추가적으로, em.getReference 후 em.find를 하게 되면, em.find()로 가져온 객체는 실제 객체가 아닌 프록시 객체를 가르키게 된다.

엔티티 원본을 반환

5. 준영속 상태일 때, 프록시를 초기화하게 되면 문제가 발생하게 된다 -> LazyInitialization 예외 발생

아까 프록시는 반드시 영속성 컨텍스트를 통해 실제 데이터를 가져온다고 했다.

근데 만약 detach() 메서드로 준영속화 해버리거나, close()로 아예 영속성 컨텍스트를 닫아버리면 어떻게 될까?

해당 객체를 준영속화 하고 프록시를 통해 초기화 하려고 시도하면 예외가 발생한다. (영속성 컨텍스트의 도움을 더이상 받을 수 없음)

즉, 반드시 프록시는 영속성 컨텍스트를 통해 초기화가 진행되기 때문에, 영속성 컨텍스트가 닫히게 되면 프록시가 target을 찾을 수 없게 된다!!!!!


프록시 연관 유틸

@Test
public void proxyUtilTest() throws Exception {
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try{
        Member member = new Member();
        member.setUsername("SIU");
        em.persist(member);

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

        // 프록시 초기화 여부 확인 -> getPersistenceUnitUtil.isLoaded()
        Member refMember = em.getReference(Member.class, member.getId()); // 프록시 초기화 안함 (메서드 암것도 사용안함)
        System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // false

        refMember.getUsername(); // 이제 초기화
        System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); //true
        
        //========================================================================================================================

        
        // 프록시 강제로 초기화 해버리기 -> Hibernate.initialize
        Hibernate.initialize(refMember);

        tx.commit();

    } catch(Exception e) {
        tx.rollback();
        e.printStackTrace();
    } finally {
        em.close();
    }
}

참고로 강제 초기화는 JPA 기술은 아니고, 하이버네이트가 지원하는 기술이다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.