개발하는 쿠키
article thumbnail

프록시 심화 주제

영속성 컨텍스트와 프록시

 

영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성(==)을 보장한다.

그럼 프록시로 조회한 엔티티의 동일성도 보장할까?

 

@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 을 가져오게 되기 때문이다.

 

 

상속관계와 프록시

상속관계와 프록시 1

프록시를 부모 타입인 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();
}

상속관계와 프록시2

직접 다운캐스팅을 하려고 해도 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)
반응형
profile

개발하는 쿠키

@COOKIE_

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!