JVM/JPA

영속성 컨텍스트와 프록시

kyoulho 2023. 7. 3. 20:39

영속 엔티티의 동일성 보장

해당 코드에 refMember와 findMember는 모두 프록시 객체로 같은 인스턴스이다.

이는 영속 엔티티의 동일성을 보장하기 위함이다.

반대로 엔티티를 먼저 조회후 프록시를 조회하면 모두 엔티티이다.

@Test
void 영속성컨텍스트와_프록시() {
    Member member = new Member("member1", "회원1");
    em.persist(member);
    em.flush();
    em.clear();
    
    Member refMember = em.getReference(Member.class, "member1");
    Member findMember = em.find(Member.class, "member1");
    
    Assertions.assertSame(refMember, findMember); // 성공
}

 

 

프록시 타입 비교

프록시로 조회한 엔티티의 타입을 비교할 때는 == 비교를 하면 안 되고 instanceof를 사용해야 한다.

@Test
void 프록시_타입비교() {
    Member member = new Member("member1", "회원1");
    em.persist(member);
    em.flush();
    em.clear();

    Member refMember = em.getReference(Member.class, "member1");
	
    // 부모 클래스와 자식 클래스를 == 비교한 것이 된다.
    Assertions.assertTrue(Member.class == refMember.getClass()); //false
    Assertions.assertTrue(refMember instanceof Member);          //true
}

 

프록시 동등성 비교

엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 equals() 메소드를 오버라이딩하고 비교하면 된다. 그런데 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 프록시면 문제가 발생할 수 있다.

equal() 메소드 안에서 객체끼리 동일성 비교를 하거나 객체의 프로퍼티의 직접 접근하여 비교를 한다면 변경해 주어야 한다.

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        // 동일성 비교
        if (this.getClass() != o.getClass()) return false;
       
        // 변경
        ig (!(o instanceof Member)) return false;

        Member member = (Member) o;
        // 객체의 프로퍼티의 직접 접근
        if (!Objects.equals(id, member.id)) return false;
        return Objects.equals(name, member.name);
        
        // 변경
        return Objects.equals(id, member.getId()) && Objects.equals(name, member.getName());
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

 

상속관계와 프록시

프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제가 있다.

때문에 instanceof 연산을 사용할 수 없으며 하위 타입으로 다운캐스팅을 할 수 없다.

 

해결 방법

JPQL

JPQL로 처음부터 자식 타입을 직접 조회해서 필요한 연산을 하면 된다. 다형성을 활용할 수는 없지만 가장 깔끔하다.

 

프록시 벗기기

하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하면 원본 엔티티를 가져올 수 있다.

이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 있다. 따라서 item == unProxyItem 은 false다.

이 방법은 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용 되지 않도록 하는 것이 중요하다. 원본 엔티티의 값을 직접 변경해도 변경 감지 기능은 동작한다.

Item item = orderItem.getIterm();
Item unProxyItem = unProxy(item);

 

비지터 패턴 사용

프록시의 메소드를 호출하여도 엔티티의 메소드를 호출하는 프록시의 구조를 이용하여 비지터 패턴을 적용한다.

새로운 기능이 필요할 때 Visitor만 추가하면 되지만 객체 구조 변경 시 모든 Visitor를 수정해야 하는 단점이 있다.

public interface Visitor{
	void visit(Book book);
    void visit(Album album);
    void visit(Movie movie);
}

public class PrintVisitor implement Visitor {
    @Override
    public void visit(Book book) {
    	System.out.println("제목 : $book.getName() 저자: $book.getAuthor()");
    }
    @Override
    public void visit(Album album) {...}
    @Override
    public void visit(Movie movie) {...}
}

// 대상 클래스
@Entity
@Ingeritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
	...
    public abstract void accept(Visitor visitor);
    ...
}

@Entity
@DiscriminatorValue("B")
public abstract class Book extends Item {
	...
    public void accept(Visitor visitor) {
    	visitor.visit(this)
    }
    ...
}

// 실행

@Test
public void 상속관계와_프록시_VisitorPattern() {
	...
    OrderItem orderItem = em.find(OrderItem.class, orderItemId);
    Item item = orderItem.getItem();
    
    //item이 프록시여도 accept 메소드는 실제 엔티티의 메소드를 실행한다.
    item.accept(new PrintVisitor());
}

'JVM > JPA' 카테고리의 다른 글

읽기 전용 쿼리의 성능 최적화  (0) 2023.07.04
N+1 문제  (0) 2023.07.04
JPA 예외 처리  (0) 2023.07.03
엔티티 그래프  (0) 2023.07.03
리스너  (0) 2023.07.02