gony-dev 님의 블로그

[JPA] 프록시와 연관관계 본문

Spring/JPA

[JPA] 프록시와 연관관계

minarinamu 2024. 11. 11. 21:44

Proxy

프록시(Proxy)는 '대리'라는 의미로 인터넷 관점에서는 내부 네트워크에서 인터넷 접속을 할 때, 빠른 액세스나 안전한 통신 등을 확보하기 위한 중계서버를 일컫는다.
클라이언트와 웹 서버의 중간에 위치하고 있기에, 이 통신을 받아주는 것을 '프록시 서버'라고 부른다.
그렇다면 JPA 관점에서의 프록시란 뭘까?

 

  • "em.find()"와 "em.getReference()", EntityManager에 대한 두 가지 메서드가 여기 있다. 이 둘은 조회라는 관점에서는 비슷하지만 조회를 하는 방식이 다르다.
  • "em.find()"는 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
  • "em.getReference()"는 데이터베이스 조회를 미루는 프록시 엔티티 객체를 조회한다.

em.getReference()

  • 해당 기능은 데이터베이스 조회 시 가짜 객체를 던져준다.
  • 프록시(가짜)의 특징
    1. 실제 클래스를 상속 받아서 만들어진다.
    2. 실제 클래스와 겉모양이 같다.
    3. 사용하는 입장에서는 진짜의 유무를 판단하지 않고 사용하면 된다!
    4. 프록시 객체는 실제 객체의 참조를 보관한다.
    5. 프록시 객체 호출 시 프록시 객체는 실제 객체의 메서드를 호출한다.
Member member = new Member();
member.setUsername("memberA");

em.persist(member);

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

Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 호출
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
Hibernate: 
    /* insert hello.jpa.Member
        */ insert 
        into
            Member
            (id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, locker_id, roleType, name) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
Hibernate: 
    select
        member0_.id as id1_4_0_,
        member0_.createdBy as createdB2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.age as age6_4_0_,
        member0_.description as descript7_4_0_,
        member0_.locker_id as locker_10_4_0_,
        member0_.roleType as roleType8_4_0_,
        member0_.team_id as team_id11_4_0_,
        member0_.name as name9_4_0_,
        locker1_.id as id1_3_1_,
        locker1_.name as name2_3_1_,
        team2_.id as id1_8_2_,
        team2_.createdBy as createdB2_8_2_,
        team2_.createdDate as createdD3_8_2_,
        team2_.lastModifiedBy as lastModi4_8_2_,
        team2_.lastModifiedDate as lastModi5_8_2_,
        team2_.name as name6_8_2_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    left outer join
        Team team2_ 
            on member0_.team_id=team2_.id 
    where
        member0_.id=?
findMember.username = memberA
  • 위의 메서드와 로그를 보면 findMember.getClass()로 확인된 객체가 Member 객체가 아니라, 하이버네이트가 강제로 만든 가짜 프록시 객체인 것을 알 수 있다.
  • 프록시 특징 Part.2
    1. 프록시 객체는 처음 사용할 때 한 번만 초기화되며, 초기화 시에는 프록시 객체가 실제 엔티티가 되는 것이 아닌 프록시 객체를 통해 실제 엔티티에 접근이 가능한 것이다.
    2. 프록시 객체는 원본 엔티티를 상속받기에 타입 체크에 유의해야 한다.
    3. 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
      • 이미 영속성 컨텍스트에 있는 객체를 굳이 다시 프록시로 감싸서 반환하는 게 의미가 없기 때문이다.
    4. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태이면, 프록시를 초기화 할 때 문제가 발생하니 주의해야 한다.

즉시 로딩과 지연 로딩

멤버를 조회할 때 연관관계로 얽혀 있는 Team이라는 엔티티가 있다면, 이를 함께 조회하는 것이 괜찮을까?
우리는 한 엔티티에 얽혀 있는 연관관계를 필요 시에 조회할지, 아니면 항시 조회 가능 상태로 만들지 정할 수 있다.
이것이 바로 즉시 로딩과 지연 로딩이다.
  • 본론부터 말하자면 필요 시에 함께 조회를 할 경우에는 지연 로딩을 사용하면 되고, 항시 조회 가능 상태로 만든다면 즉시 로딩을 사용하면 된다.

지연 로딩 LAZY

  • 지연 로딩으로 설정할 시 연관된 엔티티를 프록시로 가져온다.
  • 즉시 로딩을 사용할 경우, SQL 문제가 일어나게 되는데,
    • 즉시 로딩은 JPQL에서 N+1문제를 일으킨다.
  • 실무에서도 모든 관계에서 지연 로딩을 사용할 것을 당부하며, JPQL fetch 조인(성능 최적화 필요시)이나, 엔티티 그래프 기능을 사용하는 것을 지향하고 있다.