동일성 보장
영속성 컨텍스트는 자신이 관리하는 영속 엔티티 한에서는 동일성을 보장한다. 프록시로 조회했을 때도 마찬가지다.
예제
@Test
public void 영속성_테스트() {
Member newMember = new Member("member1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, newMember.getId());
Member findMember = em.find(Member.class, newMember.getId());
System.out.println("refMember Type = " + refMember.getClass());
System.out.println("findMember Type = " + findMember.getClass());
assertTrue(refMember == findMember);
}
refMember Type = class com.jpa.study.jpaStudy.relationship.model.Member$HibernateProxy$S6Dlk3RX
findMember Type = class com.jpa.study.jpaStudy.relationship.model.Member$HibernateProxy$S6Dlk3RX
위와 같이 조회하여 프록시 객체를 가져오고 조회를 하게 되면 영속성 컨텍스트에 같은 아이디의 Entity가 존재하기 때문에 같은 프록시 클래스를 가져오게 된다. 아래처럼 출력결과를 보면 같은 프록시 객체를 조회하기 때문에 동일성이 보장된다. 디비를 먼저 조회해도 이미 영속화된 Entity로 있기 때문에 프록시로 조회하지 않고 같은 결과를 가져오게 된다.
동등성 비교
엔티티를 비교한다고 하면 id와 같은 식별키를 통해서 동등성을 비교할 수 있지만 프록시는 이와 다르게 instanceof 통해서 비교해야한다. 정확하게는 instanceof를 통해 해당 클래스로 만들어진 프록시인지 확인하고 식별키를 비교해야한다.
@Test
public void 프록시_타입비교() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, "member1");
System.out.println("refMember Type = " + refMember.getClass());
Assert.assertFalse(Member.class == refMember.getClass()); //false
Assert.assertTrue(refMember instanceof Member); //true
}
com.jpa.study.jpaStudy.relationship.model.Member$HibernateProxy$S6Dlk3RX
위와 같이 클래스를 비교해보면 프록시의 클래스 이름을 보면 프록시를 의미하는 뒤에 $HibernateProxy... 이렇게 클래스이름이 붙게 된다.
equals()로 비교했을 때 문제점
@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String userName;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@OneToOne(mappedBy = "member")
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
public Member(String userName) {
this.userName = userName;
}
public void setTeam(Team team) {
this.team = team;
if (!team.getMembers().contains(team)) {
team.getMembers().add(this);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (this.getClass() != obj.getClass()) return false; // -- 1
Member member = (Member) obj;
if (!Objects.equals(userName, member.userName)) // -- 2
return false;
return true;
}
@Override
public int hashCode() {
return userName != null ? userName.hashCode() : 0;
}
}
위의 코드를 봤을 때 프록시로 사용한다면 2가지의 문제점을 확인해볼 수 있다.
- 서로 다른 클래스로 인식하기 때문에 위에서 클래스를 통해 비교하게되면 false값이 리턴된다.
- 프록시는 해당 클래스의 실제 메서드를 호출할 때 까지 Entity를 디비에서 가져오지 않아서 userName과 같은 멤버변수를 가져오게 되면 null을 반환한다.
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof Member)) return false; // -- 1
Member member = (Member) obj;
if (!Objects.equals(userName, member.getUserName())) // -- 2
return false;
return true;
}
따라서 위와 같이 instanceof를 통해 비교하고 프록시 객체의 메서드를 호출하여 키를 가져와서 비교하도록 정의해야한다.
상속관계와 프록시
프록시에서 부모타입을 조회했을 때 아래와 같은 문제가 발생한다.
- instanceof 연산을 사용할 수 없음
- 하위 타입으로 다운 캐스팅을 할 수 없음
@Test
public void test(){
Book book = new Book();
book.setName("jpaBook");
book.setAuthor("kim");
em.persist(book);
OrderItem saveOrderItem = new OrderItem();
saveOrderItem.setItem(book);
em.persist;
em.flush();
em.clear();
OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId());
Item item = orderItem.getItem();
System.out.println("item="+item.class());
Assert.assertFalse(item.getclass() == Book.class);
Assert.assertFalse(item instanceof Book);
Assert.assertTrue(item instanceof Item);
}
item = class jpabook.proxy.advanced.item.Item_$$_jvstffa_3
위와 같이 출력 정보를 보면 Book클래스가 아닌 Item을 기반으로 상속받아서 저장된 것을 볼 수 있다. 아래의 그림을 보면 좀 더 정확하게 이해할 수 있다.
Item을 프록시 조회했을 때 Item Entity를 가져다 사용한 것이 아닌 ItemEntity를 상속받은 프록시를 영속화 시킨 것이고 프록시 객체는 Book과는 아무런 연관관계가 되어있지 않고 Book을 상속받지도 않았기 때문에 다운캐스팅도 instanseof도 불가능하다. 이러한 문제사항은 보통 Lazy로 설정한 다형성 Entity 모델에 나온다.
프록시 문제 해결 방법
JPQL 사용
아래와 같이 JPQL을 사용하여 자식객체를 직접 조회한다면 문제가 발생할 일은 없다.
Book jpqlBook = em.createQuery("select b from Book b where b.id=:bookId", Book.class)
.setParameter("bookId"m item.getId())
.getSingleResult();
프록시 벗기기
프록시로 감춰져 있는 프록시 객체를 벗기고 원본 Entity로 업 캐스팅하여 사용한다면 해당 문제를 해결할 수 있다.
Item item = orderItem.getItem();
Item unProxyItem = unProxy(item);
if(unProxyItem instanceof Book) {
System.out.println("proxyItem instanceof Book");
Book book = (Book) unProxyItem;
System.out.println("책 저자 = " + book.getAuthor());
}
//하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
public static <T> T unProxy(Object entity) {
if (entity instanceof HibernateProxy) {
entity = ((HibernateProxy) entity)
.getHibernateLazyInitializer();
.getImplementation();
}
return (T) entity;
}
- 영속성 컨텍스트는 한번 프록시로 노출한 엔티티는 계속 프록시로 노출시킨다. 그래야지 동일성을 보장하고 클라이언트가 엔티티를 사용할 때 구분하지 않고 사용할 수 있기 때문이다.
- 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 발생한다.
item == unProxyItem //false
- 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른곳에서 사용하지 않는 것이 중요하다.
참고
HibernateProxy.getHibernateLazyInitializer(): Lazy 로딩으로 초기화하는 Entity를 가져 옵니다.
Get the underlying lazy initialization handler.
공통의 인터페이스 상속
아래처럼 공통의 인터페이스를 상속받아서 구현을 하게 되면 다형성을 활용하여 각각 다른 getTitle()이 호출하기 때문에 문제를 해결할 수 있다.
이러한 방법은 OrderItem의 코드를 수정하지 않아도 동등성 비교가 되고 클라이언트 입장에서는 해당 객체가 프록시인지 아닌지 구별하지 않고 사용이 가능하기 때문에 확장에 용이하다.
public interface TitleView {
String getTitle();
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item implements TitleView {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
//Getter, Setter
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
//Getter, Setter
@Override
public String getTitle() {
return "[제목:" + getName() + " 저자:" + author + "]";
}
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
//Getter, Setter
@Override
public String getTitle() {
return "[제목:" + getName() + " 감독:" + director + " 배우 :" + actor + "]";
}
}
비지터 패턴 사용
비지터라는 클래스를 만들고 캐스팅하는 역활을 비지터에 넘기면서 프록시에 대한 걱정없이 사용할 수 있다.
Visitor 인터페이스
public interface Visitor {
void visit(Book book);
void visit(Album album);
void visit(Movie movie);
}
Visitor 구현체
public class PrintVisitor implements Visitor {
@Override
public void visit(Book book) {
//넘어오는 book은 Proxy가 아닌 원본 엔티티
System.out.println("book.class = " + book.getClass());
System.out.println("[PrintVisitor] [제목: " + book.getName() +
"저자 :" + book.getAutor() + "]");
}
@Override
public void visit(Album album) {...}
@Override
public void visit(Movie album) {...}
}
public class TitleVisitor implements Visitor {
private String title;
public String getTitle() {
return title;
}
@Override
public void visit(Book book) {
title = "[제목:" + book.getName() + "저자:" + book.getAuthor() + "]";
}
@Override
public void visit(Album album) {...}
@Override
public void visit(Movie movie) {...}
}
대상 클래스
@Entity
@Inheritance(strategy = InheritanceType.Single_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
...
public abstract void accept(Visitor visitor);
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
//Getter, Setter
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
...
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
...
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
테스트
@Test
public void 상속관계와_프록시_VisitorPattern() {
...
OrderItem orderItem = em.find(OrderItem.class, orderItemId);
Item item = orderItem.getItem();
//PrintVisitor
item.accept(new PrintVisitor());
}
동작 과정
- item.accept()를 호출하고 상속받은 PrintVisiter를 전달
- item은 프록시이므로 먼저 프록시가 accept()를 받고 원본 엔티티의 accept()를 실행
- 원본 엔티티는 코드를 실행해 자신(this)을 visitor에 파라미터로 전달
- this가 Book 타입이므로 visit(Book book)가 실행
비지터 패턴과 확장성
//TitleVisitor
TitleVisitor titleVisitor = new TitleVisitor();
item.accept(titleVisitor);
String title = titleVisitor.getTitle();
System.out.println("TITLE=" + title);
//출력 결과
book.class = class jpabook.advanced.item.Book
TITLE=[제목:jpabook 저자:kim]
위와 같이 새로운 비지터를 추가하기만 하면 기존 코드를 변경없이 확장할 수 있는 장점이 있다.
- 장점
- 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근 가능
- instanceof와 타입캐스팅 없이 코드를 구현
- 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가
- 단점
- 너무 복잡하고 더블 디스패치를 사용하기 떄문에 이해하기 어려움
- 객체 구조가 변경되면 모든 Visitor를 수정
디스패치란?
"어플리케이션이 어떤 메소드를 호출할 것인지 결정하고 실행하는 과정
'JAVA > JPA' 카테고리의 다른 글
JPA의 엔티티 비교 (0) | 2022.11.26 |
---|---|
JPA 예외처리 (0) | 2022.11.26 |
<JPA> 네이티브 SQL 사용 (0) | 2022.10.22 |
<JPA> Criteria의 사용 방법 (0) | 2022.10.22 |
복합키와 식별관계 (1) | 2022.09.24 |