6 분 소요

📌 목차

  • 프록시 🚀
  • 즉시 로딩과 지연 로딩
  • 지연 로딩 활용
  • 영속성 전이 : CASCADE
  • 고아 객체
  • 영속성 전이 + 고아 객체, 생명주기
  • 실전 예제 - 5. 연관관계 관리

⚡ Member를 조회할 때 Team도 함께 조회해야 할까?

프록시_01

위 사진을 보면 Member, Team 객체가 존재한다. 이 때 Team 엔티티가 필요 없다면 어떻게 해야할까? 다음 예시를 시작으로 JPA에서의 프록시 개념을 공부해보자.

✅ 간단한 예시를 통한 이해

@Entity
class Member{
    @Id
    private Long id;

    // ...

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
class Team{
    @Id
    private Long id;

    @OneToMany(mappedBy = "team")
    List<Member> memberList = new ArrayList<Member>();
    // ...
}

현재 Member 엔티티와 Team 엔티티는 N : 1 연관관계 매핑으로 이루어져 있다.
다음은 해당 엔티티를 조회하는 구문을 살펴보자.

Member member = em.find(Member.class, 1L);
SELECT T.*
FROM MEMBER M
    LEFT JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE M.ID = 1;

em.find를 하는 순간 연관관계 매핑이 설정되어있는 Member, Team 엔티티의 데이터를 조인하여 가져온다. 이러한 부분은 요구되는 비즈니스에 따라 이점이 될 수도 있고 단점이 될 수 도 있다

만약 위와 같은 상황에서 Team 테이블이 아닌 Member의 테이블의 데이터만 필요하다면, 위와 같이 두 테이블이 조회되는 것은 성능 상 매우 비효율적이다. JPA는 이런 상황을 대비하여 지연 로딩이라는 기능을 제공한다.

⚡ 프록시(Proxy)

EntityManger의 getReference() 메서드를 호출하면 해당 엔티티를 바로 조회하지 않고, 실제 사용하는 시점에 조회해올 수 있다. 이러한 지연로딩을 사용하기 위해 프록시 객체를 사용한다

프록시_02

  • em.getReference()를 사용하면 가짜 객체를 반환한다.

✅ 일반적인 조회

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {
        MemberTest member = new MemberTest();
        member.setUsername("ymkim");
        em.persist(member);

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

        MemberTest findMember = em.find(MemberTest.class, member.getId());
        System.out.println("findMember = " + findMember.getId());
        System.out.println("findMember = " + findMember.getUsername());

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

위 예제는 지금까지 JPA를 통해 진행 했던 가장 일반적인 조회 방식이다.

🖨 일반적인 조회 출력 결과

Hibernate:
    call next value for hibernate_sequence
Hibernate:
    /* insert com.hello.jpatest.MemberTest
        */ insert
        into
            MemberTest
            (INSERT_MEMBER, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID)
        values
            (?, ?, ?, ?, ?, ?)
Hibernate:
    select
        membertest0_.MEMBER_ID as member_i1_9_0_,
        membertest0_.INSERT_MEMBER as insert_m2_9_0_,
        membertest0_.createdDate as createdd3_9_0_,
        membertest0_.lastModifiedBy as lastmodi4_9_0_,
        membertest0_.lastModifiedDate as lastmodi5_9_0_,
        membertest0_.TEAM_ID as team_id7_9_0_,
        membertest0_.USERNAME as username6_9_0_,
        team1_.TEAM_ID as team_id1_14_1_,
        team1_.name as name2_14_1_
    from
        MemberTest membertest0_
    left outer join
        Team team1_
            on membertest0_.TEAM_ID=team1_.TEAM_ID
    where
        membertest0_.MEMBER_ID=?
findMember = 1
findMember = ymkim

insert > select > print

위 출력 결과를 보면 데이터를 insert, select 순으로 쿼리를 날린 후에, 마지막에 조회 결과를 출력하는 것을 확인 할 수 있다. 그렇다면 프록시의 경우는 어떻게 되는지 다음 예제를 한번 살펴보자.

✅ 프록시를 통한 조회

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {
        MemberTest member = new MemberTest();
        member.setUsername("ymkim");
        em.persist(member);

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

        // 영속성 컨텍스트 초기화 후

        // ex 01. 일반적인 조회
        // MemberTest findMember = em.find(MemberTest.class, member.getId());
        // System.out.println("findMember = " + findMember.getId());
        // System.out.println("findMember = " + findMember.getUsername());

        // ex 02. 프록시를 통한 조회
        MemberTest findMember = em.getReference(MemberTest.class, member.getId()); // em.getReference => 프록시
        System.out.println("findMember = " + findMember.getId()); // id는 위에서 넣어주어서 상관 없음
        System.out.println("findMember = " + findMember.getUsername()); // username은 DB에 있는 데이터

        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
    emf.close();
}
  1. em.getReference를 하는 경우에는 쿼리를 날리지 않는다.
  2. 해당 값이 실제로 사용이 되는 시점에 쿼리가 날라간다.
    • 실제 날라가는 시점 -> getUsername()을 호출하는 경우

🖨 프록시를 통한 조회 출력 결과

Hibernate:
    call next value for hibernate_sequence
Hibernate:
    /* insert com.hello.jpatest.MemberTest
        */ insert
        into
            MemberTest
            (INSERT_MEMBER, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID)
        values
            (?, ?, ?, ?, ?, ?)
findMember = 1 // id를 먼저 출력 ⚡
Hibernate:
    select
        membertest0_.MEMBER_ID as member_i1_9_0_,
        membertest0_.INSERT_MEMBER as insert_m2_9_0_,
        membertest0_.createdDate as createdd3_9_0_,
        membertest0_.lastModifiedBy as lastmodi4_9_0_,
        membertest0_.lastModifiedDate as lastmodi5_9_0_,
        membertest0_.TEAM_ID as team_id7_9_0_,
        membertest0_.USERNAME as username6_9_0_,
        team1_.TEAM_ID as team_id1_14_1_,
        team1_.name as name2_14_1_
    from
        MemberTest membertest0_
    left outer join
        Team team1_
            on membertest0_.TEAM_ID=team1_.TEAM_ID
    where
        membertest0_.MEMBER_ID=?
findMember = ymkim // getUserName() 가 호출되는 경우 ⚡

insert > print(id) > select > print(username)

프록시(getReference())를 통해 조회를 하는 경우 결과가 달라지는 것을 확인할 수 있다. 위 출력 결과에서는 insert를 한 후에 id 값을 먼저 출력하고, 후에 username의 경우는 DB에 쿼리(Query)를 날려 데이터를 가져온 후 출력을 해주고 있다.

✅ member 객체 출력

MemberTest findMember = em.getReference(MemberTest.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
Hibernate:
    /* insert com.hello.jpatest.MemberTest
        */ insert
        into
            MemberTest
            (INSERT_MEMBER, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID)
        values
            (?, ?, ?, ?, ?, ?)
findMember : getClass() = class com.hello.jpatest.MemberTest$HibernateProxy$OBMNZa9v
findMember : getId() = 1

위 출력 결과를 살펴보면 Member 객체의 주소가 다음과 같이 출력 되고 있다.

class com.hello.jpatest.MemberTest$HibernateProxy$OBMNZa9v
  • 하이버네이트가 만든 가짜 클래스(proxy class)

✅ 프록시 특징 01

  • 실제 클래스를 상속받아 만들어짐
    • 하이버네이트 내부적으로 상속을 수행한다
  • 실제 클래스와 겉 모양이 같다
  • 사용하는 입장에서의 관점
    • 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)

✅ 프록시 특징 02

프록시_04

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

🔥 프록시 객체의 초기화

프록시_객체의_초기화

Member member = em.getReference(Member.class, "id1");
member.getName();

// 1. getReference("엔티티", "id")를 통해 프록시 객체를 획득한다.
// 2. member 참조 변수를 통해 getName()을 호출 한다.

아래 내용은 getName() 호출 시 프록시 객체가 어떻게 초기화 되는지에 대한 내용입니다.

  1. member.getName()을 호출한다.
  2. Proxty Entity target에 데이터가 존재하지 않는 것을 확인.
  3. 영속성 컨텍스트에게 초기화 요청
  4. 영속성 컨텍스트가 DB에 요청 후 DB는 실제 객체를 생성하여 반환.
  5. 영속성 컨텍스트는 DB의 반환값(응답)을 통해 실제 객체(엔티티)를 생성.
  6. MemberProxy의 Member target과 생성된 실제 객체를 매핑.
  7. 한 번 초기화가 되면 다시 요청을 할 필요가 없다.

🔥 프록시 식별자

프록시 객체는 target 변수만 가지고 있는것이 아니라, 전달받은 식별자 값도 같이 저장한다. 그러므로 아래와 같이 식별자 값만 조회할 경우 직접적인 데이터베이스 조회가 일어나지 않는다. (이러한 부분이 getUserName()의 데이터베이스 조회가 일어난 이유와 연관이 있음)

Member member = em.getRefernece(Member.class, 1);
member.getId(); // SQL 실행하지 않음

⭐ 프록시 객체 특징 정리

✅ 01. 프록시 초기화 관련

프록시 객체는 처음 사용할 때 최초로 한번 초기화 된다. 또한 프록시 객체를 초기화 하는 경우, 프록시 객체가 실제 엔티티로 바뀌는 것이 아닌 프록시 객체를 통해 실제 엔티티에 접근하는 방식으로 사용이 된다.

✅ 02. 프록시 타입 체크시 주의점

MemberTest member1 = new MemberTest();
member1.setUsername("member1");
em.persist(member1);

MemberTest member2 = new MemberTest();
member2.setUsername("member1");
em.persist(member2);

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

MemberTest m1 = em.find(MemberTest.class, member1.getId());
MemberTest m2 = em.getReference(MemberTest.class, member2.getId());

System.out.println("m1 => " + (m1 instanceof MemberTest));
System.out.println("m2 => " + (m2 instanceof MemberTest));

프록시 객체는 원본 엔티티를 상속 받는데, 타입 체크시 ‘==’ 이 아닌 instance of 메서드를 사용을 지향해야 한다.

✅ 03. 프록시 엔티티 반환 여부

MemberTest member1 = new MemberTest();
member1.setUsername("member1");
em.persist(member1);

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

MemberTest refMember = em.getReference(MemberTest.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
refMember.getUsername(); // 프록시 초기화

MemberTest findMember = em.find(MemberTest.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());

System.out.println("refMember == findMember = " + (refMember == findMember)); // 무조건 참이 나와야한다

영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출 하여도 실제 엔티티를 반환한다. 중요한 부분은 하나의 트랜잭션 단위 안에서 '=='을 사용하는 경우 영속성 컨텍스트에 엔티티가 존재 한다면 True가 반환 되어야 한다.

✅ 04. 프록시 준영속 상태인 경우

MemberTest member1 = new MemberTest();
member1.setUsername("member1");
em.persist(member1);

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

MemberTest refMember = em.getReference(MemberTest.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());

em.detach(refMember); // 영속성 관리 해제
em.close();

System.out.println("refMember = " + refMember.getUsername());
org.hibernate.LazyInitializationException: could not initialize proxy [com.hello.jpatest.MemberTest#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
	at com.hello.jpatest.MemberTest$HibernateProxy$liBlVqXs.getUsername(Unknown Source)
	at com.hello.jpatest.JpaMainTest.main(JpaMainTest.java:29)
2 19, 2022 7:00:19 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태인 경우, 프록시를 초기화하면 문제발생 위 부분은 실무에서 자주 발생하는 예외로 숙지하고 넘어가야 한다.

⭐ 프록시 확인

✅ 프록시 인스턴스의 초기화 여부 확인

PersistenceUnitUtil.isLoaded(Object entity)
MemberTest refMember = em.getReference(MemberTest.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

위와 같이 EntityMangerFactory.getPersistenceUnitUtil().isLoaded() 메서드를 호출하여 해당 프록시가 초기화 되었는지 확인이 가능하다.

✅ 프록시 클래스 확인 방법

// $HibernateProxy$El0lDtQm
entity.getClass().getName();

✅ 프록시 강제 초기화

// org.hibernate.Hibernate.initialize(entity);
Hibernate.initialize(refMember);

JPA 표준은 강제 초기화가 존재하지 않는다. 그러므로 강제 호출 member.getName()을 통해 강제 초기화를 해야한다.

💡 결론

JPA의 프록시 getReference는 실제로 많이 사용이 되지 않는다. 하지만 프록시의 메커니즘을 이해해야 지연로딩과 즉시 로딩을 이해할 수 있기 때문에 위와 같이 설명을 하였다. 다음은 지연 로딩과 즉시로딩에 대한 개념을 정리 해보자.

참고 자료

댓글남기기