[JPA] 프록시 (Proxy)

DaeHoon·2022년 3월 20일
1

JPA

목록 보기
3/5
post-thumbnail

JPA에서의 프록시

  • 어떠한 엔티티를 조회할 때 연관된 엔티티가 필요 없는 경우가 있을 수 있다.
  • 예를 들어 회원 엔티티가 팀 엔티티와 연관을 가지고 있다고 가정하고, 우리는 회원 엔티티만 출력한다고 하자. EntityManager에 있는 find 메소드를 사용할 시 회원과 연관된 팀 엔티티도 같이 조회를 할 것이다.
  • JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때 까지 데이터베이스 조회를 지연하는 방법을 사용한다. 이것을 지연 로딩이라고 한다.
  • 이 지연 로딩 기능을 사용하기 위해 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라고 한다.

프록시 (Proxy)

1. 프록시 조회

Member member1 = new Member();
member1.setName("대훈");
member1.setId(1L);
em.persist(member1); // 영속 상태
            
Member m1 = em.find(Member.class, member1.getId()); // 데이터베이스를 통한 실제 엔티티 객체 조회
Member reference = em.getReference(Member.class, member1.getId()); // 데이터베이스 조회를 미루는 프록시 엔티티 객체 생성

  • getReference() 메서드를 사용하면 진짜 객체가 아닌 하이버네이트 내부 조직에서 프록시 엔티티 객체를 반환한다.

2. 프록시 구조

  • 실제 클래스를 상속 받아서 만들어진다
  • 실제 클래스와 겉모양이 같다.
  • 사용자 입장에서는 진짜 객체인지 프록시 객체인지 구분할 필요 없이 사용하면 된다.

3. 프록시 위임

  • 프록시 객체는 실제 객체의 target을 보관한다.
  • 프록시 객체 호출 시 실제 객체의 메소드를 호출한다.

4. 프록시 객체의 초기화

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

1) getReference 메서드 호출 시, 프록시 객체를 가져온다.
2) getName 메서드 호출 시 JPA가 영속성 컨텍스트에 초기화를 요청한다.
3) 영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한다. 프록시 객체는 실제 Entity를 연결해 반환한다.
4) 그 이후 getName으로 호출 시 이미 초기화 되어있는 엔티티 객체를 반환한다.

5. 프록시 특징

Member member1 = new Member();
member1.setName("대훈");
member1.setId(1L);
em.persist(member1);

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(m1 == reference);
m1 = class com.example.jpalearning.Member
reference = class com.example.jpalearning.Member
true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference 메서드로 호출해도 실제 엔티티를 반환한다.
  • 반대로 getReference 메서드로 프록시 객체를 가지고 있으면 find 메서드 호출 시 프록시 객체를 반환한다.
  • 준영속 상태일 시, 프록시를 초기화하면 hibernate.LazyInitializationException 발생.

즉시 로딩과 지연 로딩

1. 지연 로딩 (Lazy)

  • 특정한 엔티티를 조회할 때 그 엔티티와 연관된 엔티티들도 조회할 필요가 있을까?
  • 로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.
  • 후에 실제 Team을 사용하는 시점에 초기화가 되어 DB에 쿼리를 날리게 된다.
    • getTeam 메서드로 Team을 조회하면 프록시 객체가 조회가 된다.
    • getTeam.getXXX()으로 Team의 필드에 접근할 때, 쿼리가 날아간다.
public class Member {
    @Id
    @Column(name = "MEMBER_ID", nullable = false)
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
Team team1 = new Team();
team1.setId(1L);
team1.setName("대훈드림");
em.persist(team1);

Member member1 = new Member();
member1.setName("대훈");
member1.setTeam(team1);
member1.setId(1L);
em.persist(member1);

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

Member findMember = em.find(Member.class, member1.getId());

System.out.println(findMember.getTeam().getClass());
System.out.println(findMember.getTeam().getName());
    select
        member0_.MEMBER_ID as member_i1_0_0_,
        member0_.USERNAME as username2_0_0_,
        member0_.TEAM_ID as team_id3_0_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_0_0_,
        member0_.USERNAME as username2_0_0_,
        member0_.TEAM_ID as team_id3_0_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
        

class com.example.jpalearning.Team$HibernateProxy$3r73QETY


  select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
        
대훈드림

대부분의 비즈니스 로직에서 Member와 Team을 같이 사용한다면?

  • Lazy 로딩 사용 시, Select 쿼리가 두 번 날아가게 된다.
  • 이 때는 즉시 로딩(Eager) 전략을 사용해서 같이 조회한다.

2. 즉시 로딩 (Eager)

  • 대부분의 JPA 구현체는 가능하면 조인을 사용해 한번에 엔티티를 조회하는 방식을 사용. 이렇게 하면 실제 조회할 때 한방 쿼리로 모두 조회를 해온다.
  • 아래 로그를 보면 Team 객체도 프록시가 아닌 엔티티 객체인 것을 볼 수 있다.
public class Member {
    @Id
    @Column(name = "MEMBER_ID", nullable = false)
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
Team team1 = new Team();
team1.setId(1L);
team1.setName("대훈드림");
em.persist(team1);

Member member1 = new Member();
member1.setName("대훈");
member1.setTeam(team1);
member1.setId(1L);
em.persist(member1);

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

Member findMember = em.find(Member.class, member1.getId());

System.out.println(findMember.getTeam().getClass());
System.out.println(findMember.getTeam().getName());
    select
        member0_.MEMBER_ID as member_i1_0_0_,
        member0_.USERNAME as username2_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        team1_.TEAM_ID as team_id1_1_1_,
        team1_.USERNAME as username2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_0_0_,
        member0_.USERNAME as username2_0_0_,
        member0_.TEAM_ID as team_id3_0_0_,
        team1_.TEAM_ID as team_id1_1_1_,
        team1_.USERNAME as username2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
        
class com.example.jpalearning.Team
대훈드림

주의할 점

  • 실무에서는 즉시 로딩을 사용하지 말자.
  • 위의 예는 간단하지만 즉시 로딩을 적용하면 어마어마한 SQL문이 발생한다.
    • ex) @ManyToOne으로 다섯 개의 엔티티를 EAGER로 관계를 맺고 있으면 조인이 5번 일어난다.
    • 당연히 실무에서는 테이블이 5개 보다 훨씬 많으므로 지연 로딩을 사용하자.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. 지연 로딩을 사용하자!

N+1 문제

  • N+1 문제란 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개가 나가는 문제다.
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);

Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);

Member member1 = new Member();
member1.setTeam(team1);
member1.setName("dh");
em.persist(member1);

Member member2 = new Member();
member2.setTeam(team2);
member2.setName("dh2");

em.persist(member2);

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

List<Member> members = em
                    .createQuery("select m from Member m", Member.class)
                    .getResultList();

 ----------- Member 객체 호출 ------------
   /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_0_,
            member0_.USERNAME as username2_0_,
            member0_.TEAM_ID as team_id3_0_ 
        from
            Member member0_
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_0_,
            member0_.USERNAME as username2_0_,
            member0_.TEAM_ID as team_id3_0_ 
        from
            Member member0_
            
     select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
        
 ----------- Team 객체 호출 SQL을 Member 테이블의 Row 갯수만큼 쿼리를 날린다. ------------
 
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
        
    select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
        
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_1_0_,
        team0_.USERNAME as username2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
  • 먼저 2개의 Member 데이터를 가져오기 위해 1개의 Member 관련 쿼리를 날린다.
  • EAGER로 등록이 되있으므로 Member와 연관 되어 있는 Team 쿼리 2개가 생성이 된다.
  • 2 + 1 -> 총 3개의 쿼리가 생성이 된다. 만약 Member가 수천 수만명일 시, 수만 개의 쿼리를 DB에 날리게 되고, DB에 엄청난 부하를 주게 된다.

Reference

  • 자바 ORM 표준 JPA 프로그래밍
profile
평범한 백엔드 개발자

0개의 댓글