Post

프록시





참고



프록시

  • EntityManager에는 엔티티 객체를 받아오는 메서드가 두 가지 있다.
    • em.find();
    • em.getReference();
  • find()는 알다시피 테이블을 조회해서 엔티티 객체를 만들어 가져오는 메서드이다.
  • getReference()는 엔티티의 프록시 객체만을 가져다준다.
    • 실제 엔티티 객체가 아니기 때문에, 호출 직후에 쿼리가 날라가지 않는다.
  • 프록시 객체는 실제 엔티티와 똑같이 생긴 빈 껍데기 객체이다.
    • 스프링 AOP에서 본 프록시랑 비슷한 개념이다.
  • 실제 엔티티 클래스를 상속 받아 만들어진다.
    • 때문에 겉으로 노출된 동작들은 모두 같다.
    • 사용하는 입장에선 그냥 엔티티 객체를 사용한다고 느껴진다.
  • 프록시 객체는 엔티티 객체의 참조를 갖고 있다.
    • 프록시 객체는 엔티티 객체에게 실제 행동을 위임한다.
    • 예를 들어 getName()을 호출하면 연결된 엔티티 객체의 name을 반환한다.
  • 프록시 객체는 DB 접근을 최대한 뒤로 미룬다.
    • DB 접근이 필요할 때, 스리슬쩍 갔다온다.


img-description 프록시 객체 초기화 과정


  1. em.getReference(Member.class, “id1”)
    • 멤버의 프록시를 받아온다.
    • DB 조회가 일어나지 않는다.
  2. member.getName();
    • 실제 데이터가 필요한 상황이 발생했다.
  3. 프록시 객체와 연결된 엔티티가 비어있기 때문에 디비 접근이 필요하다.
  4. 이때 진짜 쿼리가 날아간다.
  5. 프록시 객체 안에 실제 엔티티가 생성된다.
  6. getName() 호출을 엔티티에게 위임하여 응답한다.


프록시의 특징

  • 프록시 객체는 처음 한 번만 초기화 과정을 가진다.
    • 위의 과정을 겪은 member 객체가 다음에 getName() 또는 다른 필드의 get 메서드가 호출돼도
    • 또 초기화를 할 필요가 없다.
  • 프록시 객체가 초기화 되었다고 해서 실제 엔티티 객체로 바뀐 것은 아니다.
    • 프록시 객체를 통해서 프록시 객체와 연결된 엔티티 객체에 접근하는 것이다.
  • 프록시 객체는 엔티티 클래스를 상속 받은 클래스이다.
    • 타입 비교시에 == 대신 instanceOf 를 사용해야 한다.
  • 영속성 컨텍스트 내에 이미 Member 엔티티가 존재 한다면
    • getReference()를 호출해도 실제 엔티티를 반환한다.
    • 이때 주의할 것은 프록시 타입이 아닌 실제 엔티티라는 것이다.
    • == 비교가 가능
  • 초기화 되지 않는 프록시 객체가 준영속 상태일 때 DB 접근을 시도하면 예외가 발생한다.
    • org.hibernate.LazyInitializationException 예외가 발생

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    • emf.getPersistenceUnitUtil()로 PersistenceUnitUtil 객체를 받아올 수 있다.
    • persistenceUnitUtil.isLoaded(entity);
      • 초기화 여부를 boolean 타입으로 반환한다.
  • 프록시 클래스 확인
    • proxy.getClass().getName();
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(proxy);
    • JPA 표준에는 강제 초기화가 없다.
    • 강제 호출은 가능 proxy.getColumn();





즉시로딩과 지연로딩

  • JPA의 엔티티는 객체의 참조를 통해 연관 관계를 구현한다.
  • 어떤 엔티티를 조회했을 때 그 엔티티와 연관 관계를 맺은 엔티티의 조회는 어떻게 이뤄질까
  • JPA에는 두 가지 선택지가 있다.
    • 즉시 로딩
    • 지연 로딩



즉시 로딩

  • Eager loading
  • 어떤 엔티티를 조회했을 때 연관 관계인 다른 테이블도 함께 조인하여 가져오는 방식이다.
  • 연관 관계 매핑 어노테이션의 fetch 속성을 FetchType.EAGER로 줘서 설정할 수 있다.
    • @ManyToOne, @OneToOne은 디폴트가 즉시 로딩이다.
    • 하나짜리 애들은 가져와도 부담이 없다고 생각해서 저렇게 정한듯


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
class Book {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.EAGER)
    private Bookshelf bookshelf;
}

// ...

Book book = new Book();
book.setTitle("book");

Bookshelf bookshelf = new Bookshelf();
bookshelf.setName("bookshelf");

book.setBookshelf(bookshelf);

em.persist(book);
em.persist(bookshelf);

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

Book foundBook = em.find(Book.class, 1L);

System.out.println(foundBook.getBookshelf().getClass());

// ...


img-description 즉시 로딩 결과


  • book 엔티티의 bookshelf가 제대로 된 엔티티 객체를 참조한 것을 알 수 있다.



지연 로딩

  • Lazy loading
  • 어떤 엔티티를 조회했을 때 연관 관계인 다른 테이블은 조회하지 않는 방식이다.
  • 연관 관계 매핑 어노테이션의 fetch 속성을 FetchType.LAZY로 줘서 설정할 수 있다.
    • @OneToMany, @ManyToMany는 디폴트가 지연 로딩이다.
    • 여러개 있는 애들인 확실히 부담되서 이렇게 정한듯


1
2
3
4
5
6
7
8
9
10
11
@Entity
class Book {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    private Bookshelf bookshelf;
}


img-description 즉시 로딩 결과


  • 이번에는 Bookshelf의 fetch 속성을 LAZY로 줬다.
    • 조회 코드는 같다.
  • book 엔티티의 bookshelf가 프록시 객체인 것을 볼 수 있다.
    • 위에서 살펴본대로 bookshelf를 확인하려하면 그때서야 조회해서 값을 채운다.





N + 1

  • 한 번의 조회 명령으로 N개의 쿼리를 더 날리게 된다는 뜻의 이름이다.
  • 만약 각각 책이 꽂혀있는 책장이 N개 있고, 즉시 로딩으로 설정하여 모든 책장을 조회하면 어떤 쿼리가 날라갈까
    • 모든 bookshelf를 조회하는 쿼리
      • select * from bookshelf;
    • 첫 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
    • 두 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
    • N 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
  • 한 번의 조회를 위해서 부가적으로 N개의 쿼리가 더 날아갔다. 이것을 N+1 문제라고 한다.


지연 로딩

  • 그럼 지연 로딩으로 설정했다면 어떤 쿼리가 날아갈까
    • 모든 bookshelf를 조회하는 쿼리
  • 이게 끝이다.
  • 이는 문제 해결이 아니라 문제를 뒤로 미루는 방법이다. 결국엔 N개의 쿼리가 추가로 더 필요할 것이다.


이유가 뭘까?

  • db의 sql문은 외래키를 통해 연관 관계를 관리하기 때문에
    • select * from Bookshelf 이 쿼리 하나로 모든걸 알 수 있다.
  • 하지만, jpa는 객체 참조를 통해 연관 관계를 관리하기 때문에
    • Bookshelf에 대한 연관 관계인 Book 테이블의 조회가 추가로 필요하다.


fetch join

  • fetch join은 엔티티를 조회하면 연관 엔티티까지 한 번에 같이 가져오는 방법이다.

select bs from Bookshelf bs join fetch bs.bookList


img-description fetch join query


  • 연관 엔티티의 테이블을 조인한다.
  • 하나의 쿼리로 연관 엔티티의 컬럼까지 가져왔다.
  • 저 하나의 쿼리로 끝이다.
  • 그럼 그냥 join으로는 해결이 안되는가

select bs from Bookshelf bs join bs.bookList


img-description join query


  • 마찬가지로 연관 엔티티의 테이블을 조인한다.
  • 그러나 연관 엔티티의 컬럼을 select하지 않는다.
  • 따라서 연관 엔티티의 컬럼을 조회하기 위한 쿼리가 추가로 발생한다. (N+1 발생)
    • 사진에는 없지만 뒤에 bookshelf의 개수만큼 쿼리가 추가로 발생한다.


fetch join의 문제점

  • fetch join은 limit을 적용할 수 없다.
  • getResultMax()를 사용할 수 있고 잘 동작하기는 한다.
  • 그러나 모든 쿼리를 db로부터 가져온 뒤에, 메모리에서 페이징 처리를 한다.
  • 결과적으로 페이징 처리를 위한 오버헤드가 발생한다.



@EntityGraph

  • Spring Data Jpa의 리포지토리에서 fetch 조인을 위해 사용하는 어노테이션이다.
  • 내용은 추후에 더 추가



@Batch Size

  • 내용 추후에 더 추가





This post is licensed under CC BY 4.0 by the author.