ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프록시
    JPA/JPA 기본 2023. 7. 11. 18:05
     public class JpaMain {
        public static void main(String[] args) {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory();
    
            EntityManager em = emf.createEntityManager();
    
            EntityTransaction tx = em.getTransaction();
            tx.begin();
            
     	try {
                Member member = em.find(Member.class, 1L);
                
                // printMember(member);
                
                printMemberAndTeam(member);
    
                tx.commit();
            } catch (Exception e) {
                tx.rollback();
            } finally {
                em.close();
            }
    
            emf.close();
        }
    
        private static void printMember(Member member) {
            System.out.println("username = " + member.getName());
        }
    
        private static void printMemberAndTeam(Member member) {
            String username = member.getName();
            System.out.println("username = " + username);
    
            Team team = member.getTeam();
            System.out.println("team = " + team.getName);
        }
    }

    printMember와 printMemberAndTeam이라는 함수가 있다고 가정을 하자. Member와 Team을 같이 호출해야 할 때와 Member만 호출하고 싶을 때가 있는데 연관관계가 맺어있다고 해서 쿼리가 계속 같이해서 나가면 성능 상에 문제가 있을 것이다.

     

     

     

    프록시 기초

    JPA에서는 em.find() 말고도 em.getReference()라는 메서드가 있다.

    • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

    em.getReference()는 쿼리가 나가지 않지만 객체가 조회되는 것이다.

     

    사진출처 : 김영한님 인프런 강의

    Member findMember = em.getReference(Member.class, member.getId());
    
    System.out.println("findMember.id = " + findMember.getId());
    System.out.println("findMember.name = " + findMember.getName());

    맨 윗줄의 findMember를 가져오는 데에는 쿼리가 호출되지 않는다. em.getReference()이기 때문이다.

     

    두 번째 줄에서 getId()를 출력할 때에도 쿼리가 호출되지 않는데 세 번째 줄인 getName()을 출력할 때에는 쿼리가 호출된다. 그 이유는 em.getReference()를 할 때 member.getId()는 파라미터로 넘겨줬지만 name은 넘겨주지 않았기 때문이다.

     

    아래 사진과 같이 영속성 컨텍스트에 초기화를 요청하고, 영속성 컨텍스트가 DB 조회를 통해 실제 Entity를 생성하며, MemberProxy가 실제 Entity의 name을 target.getName()으로 불러오는 것이다.

    사진출처 : 김영한님 인프런 강의

     

     

    프록시의 특징

    1. 프록시 객체는 처음 사용할 때 한 번만 초기화
    2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니고, 초기화가 되면서 프록시 객체를 통해서 실제 엔티티에 접근 가능해짐
    3. 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의
    4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
    5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화 시 문제 발생

     

     

     

    네 번째 줄의 말이 잘 이해가 안가는데

    Member member1 = new Member();
    member1.setName("member1");
    em.persist(member1);
    
    em.flush();
    em.clear();
    
    Member m1 = em.find(Member.class, member1.getId());
    System.out.println("m1 = " + m1.getClass());
    
    Member reference = em.getReference(Member.class, member1.getId());
    System.out.println("reference = " + reference.getClass());
    
    System.out.println("a == a: " + (refMember == findMember));

    이렇게 출력을 해 보면 m1은 Member 객체가 당연히 나오는데, reference도 프록시가 아니라 Member 객체가 나온다.

    (프록시라면 class.hellojpa.Member가 아니라 class hellojpa.Member$HibernateProxy$~~~ 등으로 나옴)

     

    이렇게 나오는 이유는 첫 번째는 이미 1차 캐시에 있는 데이터를 굳이 프록시로 가져오는 데 이점이 없기 때문에 원본을 가져오는 것이다.

    두 번째 이유가 중요한데, JPA에서는 트랜잭션 안에서 한 영속성 컨텍스트에서 가져 온 데이터는 항상 똑같음을 보장해주기 때문에 이미 원본을 사용 중인 트랜잭션에서는 em.getReference()를 쓰더라도 원본을 가져온다는 것이다.

     

    맨 마지막 줄을 JPA는 항상 충족시켜야한다.  (a == a)

     

     

    Member member1 = new Member();
    member1.setName("member1");
    em.persist(member1);
    
    em.flush();
    em.clear();
    
    Member refMember = em.getReference(Member.class, member1.getId());
    System.out.println("refMember = " + refMember.getClass());
    
    Member findMember = em.find(Member.class, member1.getId());
    System.out.println("findMember = " + findMember.getClass());
    
    System.out.println("refMember == findMember: " + (refMember == findMember));

    위에서 설명 했듯이 == 비교를 충족시켜야 하기 때문에 em.getReference()로 호출을 하면 em.find()로 호출을 하더라도 프록시가 호출된다.

     

     

     

     

     

    다섯 번째 줄의 내용은 실무에서 많이 만나는 문제라고 한다.

    Member member1 = new Member();
    member1.setName("member1");
    em.persist(member1);
    
    em.flush();
    em.clear();
    
    Member refMember = em.getReference(Member.class, member1.getId());
    System.out.println("refMember = " + refMember.getClass());
    
    em.clear(); // 여기서 영속성 컨텍스트가 종료되어서 아래에서 초기화 불가능
    
    refMember.getUsername();

    영속성 컨텍스트 상태여야지만 DB에서 조회를 해서 초기화를 시켜주는데, 만약에 영속성 컨텍스트 상태가 아니게 된다면 초기화를 할 수 없게 된다.

     

    LazyInitializationException 예외가 터지게 된다면 영속성 컨텍스트를 다시 확인해보자.

    'JPA > JPA 기본' 카테고리의 다른 글

    영속성 전이(CASCADE)  (0) 2023.07.11
    즉시로딩과 지연로딩  (0) 2023.07.11
    상속관계 매핑  (0) 2023.07.11
    다양한 연관관계 매핑  (0) 2023.07.11
    연관관계 매핑 - 단방향, 양방향 연관관계  (0) 2023.07.11
Designed by Tistory.