[Spring] JPA Proxy & FetchType.LAZY,EAGER

Gogh·2023년 1월 2일
0

Spring

목록 보기
16/23

🎯 목표 : Spring DATA JPA에서 프록시와 연관관계 관리 방법 학습

📒 Proxy


📌 프록시의 특징

  • 객체는 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기 어렵다. JPA에서는 이 문제를 해결하기 위해 프록시라는 기술을 사용한다.
  • 하나의 객체를 조회할때 처음부터 실제 객체를 데이터베이스에서 가져오는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회해서 가져오는 것을 도와 준다.
  • 즉 실제 클래스를 상속받은 가짜 프록시 객체를 조회만 하여 가지고 있다가 실제 해당 객체의 정보에 접근하는 시점에 데이터베이스로 쿼리문을 날려 객체를 가져온다.
  • 실제 객체의 참조를 하는 방식으로 프록시 객체를 보관한다.
  • 자주 사용하는 객체는 조인을 사용하여 함께 조회하는 것이 효과적이다. JPA 에서는 즉시 로딩과 지연 로딩 기능을 지원한다.

매핑관계 결과

  • 연관 관계 매핑 블로깅 예제다.
  • 멤버와 포인트의 관계를 확인해 보면, 멤버의 객체가 필요하다 하여, 멤버가 필요할때 마다 포인트 객체가 필요한 것은 아니다.
  • 해당 멤버가 어느정도의 적립 포인트가 있는지 조회하고자 할때 포인트 객체의 데이터가 필요한 것이다.
  • 하지만, 멤버의 객체가 필요할때 데이터베이스에 조회를 하게되면, 맴버의 테이블에 대하여 Select 문을 날리게 될 것이고, 멤버에서 가지고 있는 포인트의 외래키로 포인트 테이블과 Join 하여 쿼리문을 날리게 된다.
  • 단순한 예제라 쿼리문을 실제로는 한번만 날리게 되지만 복잡하고 수많은 엔티티들이 복잡한 연관관계를 가지고 있다면 멤버 하나만 조회했는데 불필요한 쿼리문까지 날아가는 상황이 발생할 것이다.
  • 이럴때 프록시 기술을 사용해 포인트 객체를 프록시로 할당하여 실제 포인트 객체의 데이터가 필요할때 호출하는 방법을 사용할수 있을 것이다.
  • 프록시에 따른 두 객체간 관계는 지연 로딩과 즉시 로딩에서 확인 해 보기로 하고, 프록시 객체의 존재 여부와 프록시 객체가 실제로 언제 데이터베이스의 객체로 변하게 되는지 확인해 보자. 정확히 말하면, 변하는 것은 아니다.

📌 프록시 객체 분석

  • 예제 실습 환경 : Gradle, H2DB
@Configuration
public class JpaMain {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            Member member = new Member();
            member.setName("abc");
            member.setEmail("aaa@gmail.com");
            member.setPhone("010-5555-5555");
            Point point = new Point();
            point.setPointCount(5);
            member.setPoint(point);
            em.persist(member);
            tx.commit();
            em.clear();

            Member refMember = em.getReference(Member.class, member.getMemberId());
            System.out.println("refMember = " + refMember.getClass());
            System.out.println("==================================================");
            System.out.println("refMember.getMemberId() = " + refMember.getMemberId());
            System.out.println("==================================================");
            System.out.println("refMember.getEmail() = " + refMember.getEmail());
            System.out.println("==================================================");
            System.out.println("refMember = " + refMember.getClass());

        };
    }
}
  • 엔티티 매핑과 코드 작성은 이미 되어 있다고 가정하고 프록시 객체를 확인하기 위한 예제 코드만 확인 해 보자. 연관 관계 매핑 블로깅에 엔티티 매핑 코드를 확인할 수 있다.
  • 위 예제에서 새로운 맴버와, 포인트 객체를 생성하여 멤버에 포인트 객체를 셋팅 해준 후 persist()로 멤버를 영속화 시켜 주고 commit()까지 완료 하였다.
  • 영속성 컨텍스트를 clear()로 초기화 후 getReference()를 호출하여 프록시 객체를 가져 왔다.
    • 영속성 컨텍스트 1차 캐시에 데이터가 있다면 getReference()해도 영속화 된 실제 엔티티 데이터를 가져온다.
  • 예제 코드의 출력값을 확인 해 보자.

image

  • 출력값을 보면 엔티티들의 Create와 Insert 쿼리문은 다 정상적으로 날아간 상태고, 예제 코드에서 출력부만 확인 했다.
  • 첫번째 줄의refMember = class com.example.mappingprac.domain.Member$HibernateProxy$jpogeW8z를 살펴 보면 getReference()를 호출하여 HibernateProxy 객체가 할당 된 것을 볼수 있다.
  • refMember.getMemberId()가 호출되는 시점에는 쿼리문이 날아가지 않고 데이터가 출력되는 것을 확인 할수 있는데, 이는 프록시로 엔티티를 조회할때, PK 값은 파라미터로 전달되어 프록시 객체를 식별할수 있는 식별자 값으로 보관하기 때문 이다.
  • 다음으로, refMember.getEmail()를 호출하는 시점에 쿼리문이 날아간 것을 볼수 있는데,
  • 마지막 줄의 refMember = class com.example.mappingprac.domain.Member$HibernateProxy$jpogeW8z를 살펴 보면 첫번째 줄의 내역과 같은 것을 확인 할수 있다.
  • 프록시 객체는 실제 엔티티를 상속받은 가짜 객체로 실제 데이터에 접근할때 데이터베이스에서 가져온 엔티티 객체로 변경된다고 했다.
  • 하지만, 실제 데이터에 접근 하고 나서도 프록시 객체는 그대로 유지되고 있다.
  • 즉, 프록시 객체는 실제 엔티티를 참조하고 있는 것이며, 실제 데이터에 접근하더라도 참조하고 있는 엔티티 객체가 프록시 객체를 대체하지 않는다는 것을 확인할수 있다.
  • 참고로, 프록시 객체의 초기화 여부는 PersistenceUnitUtil.isLoaded(Object object) 메소드를 사용하여 boolean 타입으로 조회 할수 있다.

📒 FetchType.LAZY / EAGER


📌 엔티티 예제

  • 즉시 로딩과 지연 로딩을 살펴보기 위해 아래 엔티티를 가정하고 확인 하자.
// Member.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long memberId;

  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  private String email;

  @Column(nullable = false)
  private String phone;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "point_id")
  private Point point;
}

// Point.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Point extends Audit{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long pointId;

  private int pointCount;
}

📌 즉시 로딩

  • 즉시 로딩은 엔티티를 조회 할때 연관된 엔티티도 함께 조회 된다.
  • 즉시 로딩을 사용하기 위해서는 @OneToOne(fetch = FetchType.EAGER)와 같이 에트리뷰트를 설정 해 주면 된다.
  • @XxxToOne 어노테이션은 default 값이 EAGER 라서 설정할 필요가 없다.
  • 그렇다면, 현재 예제 엔티티 멤버와 엔티티 포인트 와의 연관 관계는 즉시 로딩으로 기본값이다.
  • 프록시에서 사용한 예제 코드의 쿼리문이 날아간 부분을 다시 살펴 보면 아래와 같다.

image

  • 멤버의 데이터만 조회했을 뿐인데, Select 문이 날아가고 밑에 Join 문이 같이 날아가서 포인트 엔티티를 함께 호출하고 있다.
  • 즉시 로딩은 엔티티를 조회 할때 연관된 엔티티까지 한번에 모두 조회하는 것을 확인 할수 있다.

📌 지연 로딩

  • 지연 로딩은 연관된 엔티티를 실제 사용할 때 조회 한다.
  • 지연 로딩을 사용하기 위해서는 @OneToOne(fetch = FetchType.LAZY)와 같이 에트리뷰트를 설정 해 주면 된다.
  • @XxxToMany 어노테이션은 default 값이 LAZY다.
  • 바로 코드에 적용하고 프록시 예제를 만들어 쿼리문을 살펴 보았다.
//Member.java
//........
    @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    @JoinColumn(name = "point_id")
    private Point point;
//......

//JpaMain.java
@Configuration
public class JpaMain {
  private EntityManager em;
  private EntityTransaction tx;

  @Bean
  public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
    this.em = emFactory.createEntityManager();
    this.tx = em.getTransaction();

    return args -> {
      tx.begin();
      Member member = new Member();
      member.setName("abc");
      member.setEmail("aaa@gmail.com");
      member.setPhone("010-5555-5555");
      Point point = new Point();
      point.setPointCount(5);
      member.setPoint(point);
      em.persist(member);
      tx.commit();
      em.clear();

      System.out.println("============== FindMember ===================");
      Member findMember = em.find(Member.class, member.getMemberId());
      System.out.println("findMember.Point = " + findMember.getPoint().getClass());
      System.out.println("==================================================");
      System.out.println("refMember.getMemberId() = " + findMember.getPoint().getPointCount());
      System.out.println("==================================================");
      System.out.println("findMember.Point = " + findMember.getPoint().getClass());
    };
  }
}
  • @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)로 포인트 객체를 참조하고 있는 필드에 에트리뷰트를 추가 해 주었다.
  • 출력 결과를 확인 해보자.

image

  • 출력 결과를 확인해 보면, em.find(Member.class, member.getMemberId());를 호출할떄, 멤버 테이블에 쿼리문이 한번 날아가고 프록시 예제와 다르게 포인트 테이블에 대한 조인 쿼리문은 날아가지 않았다.
  • findMember가 가지고 있는 포인트 객체를 확인해보면 findMember.Point = class com.example.mappingprac.domain.Point$HibernateProxy$nC7VQr4h로 프록시 객체를 가지고 있다.
  • findMember.getPoint().getPointCount()를 호출할때 포인트 테이블에 쿼리문이 날아가는 것을 확인할 수 있다. 지연 로딩의 쿼리문 호출 시점이다.
  • findMember가 가지고 있는 포인트 객체는 프록시 객체로, 실제 데이터를 조회하고 나서도 여전히 프록시 객체인 것을 확인 할수 있다.
  • 두 엔티티 간의 관계가 아주 단순하기 때문에 지연 로딩을 적용하는 것이 비효율적으로 보이지만, 복잡하고 수많은 엔티티가 있다면, 상황에 따라 지연 로딩을 적용하는 것은 필수다.

📌 정리

  • 단순한 엔티티 연관 관계에서는 필요성을 확인할 수 없지만, 복잡한 엔티티 관계에서는 필히 지연 로딩을 설정하는 것이 좋다.

  • 즉시 로딩을 사용하게 되면 예상하지 못한 Join 쿼리문이 날아가게 된다.

    • 만약 포인트 객체에서 다른 엔티티로 추가로 참조를 하고 있다고 가정하고 즉시 로딩으로 에트리뷰트가 설정 되어 있다면, 포인트 엔티티를 가지고 와서 다시 참조하고 있는 엔티티들을 조회하는 쿼리문을 추가로 또 날리게 될 것이다.
  • 즉시 로딩은 동적 쿼리문을 사용하거나 복잡한 쿼리문을 날릴떄 N+1 문제를 야기한다.

    • JPQL에서는 입력받은 쿼리문이 그대로 SQL로 변환되기 때문에 만약 멤버만 데이터베이스에서 가져오는 JPQL을 날리면, 멤버를 가지고 와서 보니 포인트 객체가 필요하다고 판단되어 다시 쿼리문을 날려 필요한 엔티티를 가져오게된다.
    • 만약, 즉시 로딩으로 멤버 데이터가 5개가 있고, 포인트 데이터도 5개가 있다고 가정했을때, 멤버 리스트를 받기위해 멤버 테이블만 가져오는 JPQL을 날렸을때, 5개의 리스트를 가지고 와서 보니 포인트 객체가 필요하여 포인트 데이터를 가져오는 쿼리문을 다시 날리게 될 것이다.
    • 이때, 5개의 포인트 데이터를 리스트로 가져오는 것이 아니라, 각각의 포인트 데이터를 조회하게 된다. 쿼리 1개를 날렸는데 5개의 쿼리가 추가로 날아가게 되는 것이다.
    • 아래 예제에서 문제점을 확인해 보자.
  • 코드가 길어 아래 블럭을 확인을 하면 된다.


✅ JpaMain.java 예제
@Configuration
public class JpaMain {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            Member member = new Member();
            member.setName("abc");
            member.setEmail("aaa@gmail.com");
            member.setPhone("010-5555-5555");
            Member member1 = new Member();
            member1.setName("bbb");
            member1.setEmail("bbb@gmail.com");
            member1.setPhone("010-8888-5555");
            Member member2 = new Member();
            member2.setName("ccc");
            member2.setEmail("ccc@gmail.com");
            member2.setPhone("010-3333-5555");
            Member member3 = new Member();
            member3.setName("ddd");
            member3.setEmail("ddd@gmail.com");
            member3.setPhone("010-4444-5555");
            Member member4 = new Member();
            member4.setName("eee");
            member4.setEmail("eee@gmail.com");
            member4.setPhone("010-1234-5555");
            Point point = new Point();
            Point point1 = new Point();
            Point point2 = new Point();
            Point point3 = new Point();
            Point point4 = new Point();
            point.setPointCount(5);
            point1.setPointCount(4);
            point2.setPointCount(3);
            point3.setPointCount(2);
            point4.setPointCount(1);
            member.setPoint(point);
            member1.setPoint(point1);
            member2.setPoint(point2);
            member3.setPoint(point3);
            member4.setPoint(point4);


            em.persist(member);
            em.persist(member1);
            em.persist(member2);
            em.persist(member3);
            em.persist(member4);
            tx.commit();
            em.clear();

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

        };
    }
}
✅ 출력물 확인

image

  • 멤버 엔티티의 포인트 객체 참조를 즉시 로딩으로 바꾸고em.createQuery("select m from Member m", Member.class)를 이용하여 쿼리문을 날리면 출력물과 같이 Select 문이 한번 날아가고 5개의 포인트 Select 문이 날아가는 것을 확인할 수 있다.
  • 결론은, 기본 fetch 전략은 지연 로딩으로 설정하면 된다.
  • 위 경우 지연 로딩일때 두번의 쿼리를 날려 데이터를 가져와야 되는데 이는 JPQL의 fetch join을 통해 하나의 쿼리문만 날려 가져올수 있으니 지연 로딩을 사용하고 상황에 따라 대응하는 방식으로 작성하자.
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글