프록시
참고
프록시
- EntityManager에는 엔티티 객체를 받아오는 메서드가 두 가지 있다.
- em.find();
- em.getReference();
- find()는 알다시피 테이블을 조회해서 엔티티 객체를 만들어 가져오는 메서드이다.
- getReference()는 엔티티의 프록시 객체만을 가져다준다.
- 실제 엔티티 객체가 아니기 때문에, 호출 직후에 쿼리가 날라가지 않는다.
- 프록시 객체는 실제 엔티티와 똑같이 생긴 빈 껍데기 객체이다.
- 스프링 AOP에서 본 프록시랑 비슷한 개념이다.
- 실제 엔티티 클래스를 상속 받아 만들어진다.
- 때문에 겉으로 노출된 동작들은 모두 같다.
- 사용하는 입장에선 그냥 엔티티 객체를 사용한다고 느껴진다.
- 프록시 객체는 엔티티 객체의 참조를 갖고 있다.
- 프록시 객체는 엔티티 객체에게 실제 행동을 위임한다.
- 예를 들어 getName()을 호출하면 연결된 엔티티 객체의 name을 반환한다.
- 프록시 객체는 DB 접근을 최대한 뒤로 미룬다.
- DB 접근이 필요할 때, 스리슬쩍 갔다온다.
- em.getReference(Member.class, “id1”)
- 멤버의 프록시를 받아온다.
- DB 조회가 일어나지 않는다.
- member.getName();
- 실제 데이터가 필요한 상황이 발생했다.
- 프록시 객체와 연결된 엔티티가 비어있기 때문에 디비 접근이 필요하다.
- 이때 진짜 쿼리가 날아간다.
- 프록시 객체 안에 실제 엔티티가 생성된다.
- 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());
// ...
- 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;
}
- 이번에는 Bookshelf의 fetch 속성을 LAZY로 줬다.
- 조회 코드는 같다.
- book 엔티티의 bookshelf가 프록시 객체인 것을 볼 수 있다.
- 위에서 살펴본대로 bookshelf를 확인하려하면 그때서야 조회해서 값을 채운다.
N + 1
- 한 번의 조회 명령으로 N개의 쿼리를 더 날리게 된다는 뜻의 이름이다.
- 만약 각각 책이 꽂혀있는 책장이 N개 있고, 즉시 로딩으로 설정하여 모든 책장을 조회하면 어떤 쿼리가 날라갈까
- 모든 bookshelf를 조회하는 쿼리
- select * from bookshelf;
- 첫 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
- 두 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
- …
- N 번째 bookshelf에 꽂혀있는 book을 조회하는 쿼리
- 모든 bookshelf를 조회하는 쿼리
- 한 번의 조회를 위해서 부가적으로 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
- 연관 엔티티의 테이블을 조인한다.
- 하나의 쿼리로 연관 엔티티의 컬럼까지 가져왔다.
- 저 하나의 쿼리로 끝이다.
- 그럼 그냥 join으로는 해결이 안되는가
select bs from Bookshelf bs join bs.bookList
- 마찬가지로 연관 엔티티의 테이블을 조인한다.
- 그러나 연관 엔티티의 컬럼을 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.