프록시 심화 주제
영속성 컨텍스트와 프록시
영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성(==)을 보장한다.
그럼 프록시로 조회한 엔티티의 동일성도 보장할까?
@Test
public void 영속성컨텍스트와_프록시1() {
Team team1 = Team.builder()
.name("team1")
.build();
em.persist(team1);
em.flush();
em.clear();
Team refTeam = em.getReference(Team.class, team1.getId()); // 프록시를 이용한 방법
Team findTeam = em.find(Team.class, team1.getId()); // 영속성 컨텍스트 이용한 방법
System.out.println("refTeam = " + refTeam.getClass()); // Team$HibernateProlxy$AA
System.out.println("findTeam = " + findTeam.getClass()); // Team$HibernateProlxy$AA
assertTrue(refTeam == findTeam); // 성공
}
Team 엔티티를 프록시를 이용해서 조회(refTeam)하고, 영속성 컨텍스트를 이용해서 조회(findTeam)했다.
refTeam 과 findTeam의 Class 정보는 일치한다. 주소값까지 비교하는 동일성이 보장된다.
영속성 컨텍스트는 영속 엔티티의 동등성을 보장해야 하기 때문에 프록시로 조회된 객체를 다시 조회하라는 요청이 오면 원본이 아닌 프록시를 반환한다.
@Test
public void 영속성컨텍스트와_프록시2() {
Team team1 = Team.builder()
.name("team1")
.build();
em.persist(team1);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team1.getId()); // 영속성 컨텍스트 이용한 방법
Team refTeam = em.getReference(Team.class, team1.getId()); // 프록시를 이용한 방법
System.out.println("refTeam = " + refTeam.getClass()); // Team
System.out.println("findTeam = " + findTeam.getClass()); // Team
assertTrue(refTeam == findTeam); // 성공
}
반대로 영속성 컨텍스를 이용해서 조회하고, 프록시를 이용해 조회하면 둘 다 원본 엔티티를 얻는다.
원본 엔티티를 이미 조회해서 영속성 컨텍스트에 저장했기 때문에 나중에 프록시를 이용해 조회할 때 원본을 반환한다.
이때에도 당연히 동일성이 보장된다.
프록시 타입 비교
프록시는 원본 데이터를 상속받아서 만들어진다.
그래서 프록시와 원본 데이터의 타입 비교시 instanceof를 사용해야 한다.
동일성 비교(==) 를 하면 불일치하다고 나온다.
프록시 동등성 비교
엔티티를 equals() 메서드로 비교하려면 커스텀해야 한다.
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(obj instanceof Team)){
return false;
}
Team team = (Team) o;
if (name == null && team.getName() == null) { // getter사용
return true;
}
return name.equals(team.getName()); // getter사용
}
getter 메서드를 이용해서 멤버변수를 가져와야 한다. Team team 은 프록시 객체이기 때문에 team.name 으로 가져오면 null 을 가져오게 되기 때문이다.
상속관계와 프록시
프록시를 부모 타입인 Item으로 조회하면 문제가 발생한다.
@Test
void 상속관계와 프록시1() {
// 자식클래스 객체 생성
Book book = new Book();
book.setAuthor("kang");
em.persist(book);
em.flush();
em.clear();
// 프록시를 부모클래스 타입으로 조회
Item proxyItem = em.getReference(Item.class, book.getId());
System.out.println("proxyItem = " + proxyItem.getClass());
// proxyItem은 Item과 상속관계를 가지고 있기 때문에
// proxyItem instanceof Item이 맞다.
if (proxyItem instanceof Book) { // if문 내부 로직이 실행되지 않는다.
System.out.println("proxyItem instanceof Book");
Book book1 = (Book) proxyItem;
System.out.println("책 저자 : " + book1.getAuthor());
}
assertThat(proxyItem.getClass() == Book.class).isFalse();
assertThat(proxyItem instanceof Book).isFalse();
assertThat(proxyItem instanceof Item).isTrue();
}
직접 다운캐스팅을 하려고 해도 ClassCastException이 발생한다.
해결방법
- JPQL로 대상 직접 조회
- 프록시 벗기기
- 기능을 위한 별도의 인터페이스 제공
- 비지터 패턴 사용
성능 최적화
N+1문제
쿼리가 1번만 실행돼야 하는데 N번이 추가로 실행되는 문제다.
즉시로딩과 N+1
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member를 조회하는 쿼리를 실행했을 때, "SELECT * FROM MEMBER" 쿼리만 실행되는 것이 아니라 OneToMany로 연결된 Order 정보도 조회하는 쿼리가 Member에서 조회된 개수N만큼 추가로 실행된다.
"SELECT * FROM ORDERS WHERE MEMBER_ID = ?"
첫 번째 쿼리인 "SELECT * FROM MEMBER"를 실행했을 때 데이터 개수가 5개가 나왔다면, 두 번째 쿼리인 "SELECT * FROM ORDERS WHERE MEMBER_ID = ?"는 5번 실행된다. 그러면 총 N+1 = 5+1 = 6번이 실행된다.
지연로딩과 N+1
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
지연로딩은 단순히 위의 코드만 실행했을 때는 쿼리가 1번만 실행되기 때문에 N+1문제가 발생하지 않는다.
서비스 로직에서 getOrders()를 할 때 N+1문제가 발생한다.
for (Member member : members) {
// 지연로딩 초기화
System.out.println("member = " + member.getOrders().size());
}
N+1문제를 피할 수 있는 방법
- 페치 조인 사용
- 하이버네이트 @BatchSize
- 하이버네이트 @Fetch(FetchMode.SUBSELECT)
읽기 전용 쿼리의 성능 최적화
엔티티가 영속성 컨텍스트에서 관리되면 1차 캐시, 변경감지의 장점을 얻을 수 있다.
하지만 변경감지를 위해 스냅샷 인스턴스를 보관하므로 메모리를 많이 사용한다.
단순 조회 기능의 경우에는 성능 최적화를 위해 스냅샷을 적용하지 않을 수 있다.
// 스칼라 타입으로 조회
select o.id, o.name, o.price
from Order o
// 읽기 전용 쿼리 힌트 사용
TypedQuery<Order> query = em.createQuery("select o from Order o",
Order.class);
query.setHint("org.hibernate.readOnly", true);
// 읽기 전용 트랜잭션 사용
@Transactional(readOnly = true)
플러시 모드를 MANUAL로 설정해서 강제로 flush()를 호출하지 않는 한 flush()가 일어나지 않는다.
변경감지를 통한 스냅샷 비교도 하지 않아 성능이 향상된다.
// 트랜잭션 밖에서 읽기 = 트랜잭션 없이 조회하기
@Transactional(propagation = Propagation.NOT_SUPPORTED)
'Coding > computer science' 카테고리의 다른 글
JPA에서 Page 기능 사용하기 (0) | 2023.11.23 |
---|---|
자바 ORM 표준 JPA 프로그래밍 | 10. 객체지향 쿼리 언어 (0) | 2023.11.16 |
자바 ORM 표준 JPA 프로그래밍 | 09. 값 타입 (+ 값 타입 컬렉션 최적화 방법) (0) | 2023.10.26 |
자바 ORM 표준 JPA 프로그래밍 | 08. 프록시와 연관관계 관리 (0) | 2023.10.19 |
자바 ORM 표준 JPA 프로그래밍 | 07. 고급 매핑 (0) | 2023.10.05 |