Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션
10. 객체지향 쿼리 언어
11. 웹 애플리케이션 제작
11.1 프로젝트 환경설정
11.2 도메인 모델과 테이블 설계
11.3 애플리케이션 구현
12. 스프링 데이터 JPA
12.1~3 스프링 데이터 JPA 소개, 공통 인터페이스 기능
12.5~10 명세, 사용자 정의 리포지토리, Web 확장...
13. 웹 애플리케이션과 영속성 관리
14. 컬렉션과 부가기능
15. 고급 주제와 성능 최적화
15.3 프록시 심화 주제
- 프록시는 원본 엔티티를 상속받아서 만들어지므로 클라이언트는 구분 없이 사용 가능
- 지연 로딩을 하려고 프록시로 변경해도 비즈니스 로직을 수정하지 않아도 사용 가능
- 프록시를 사용하는 방식의 기술적인 한계로 예상치 못한 문제가 발생
15.3.1 영속성 컨텍스트와 프록시
- 영속성 컨텍스트는 영속 엔티티의 동일성을 보장
- 영속성 컨텍스트에 프록시로 사용할 때도 동일한지 확인하는 예시
@Test public void 영속성컨텍스트와_프록시() { Member newMember = new Member("member1", "회원1"); em.persist(newMember); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, "member1"); Member findMember = em.find(Member.class, "member1"); System.out.println("refMember Type = " + refMember.getClass()); System.out.println("findMember Type = " + findMember.getClass()); Assert.assertTrue(refMember == findMember); //성공 }
//출력 결과 refMember Type = class jpabook.advanced.Member_$$_jvst843_0 findMember Type = class jpabook.advanced.Member_$$_jvst843_0
- 영속성 컨텍스트는 프록시로 조회된 엔티티에 대해 같은 엔티티 조회 요청이 오면 조회된 프록시를 반환, 그것을 통해 엔티티의 동일성을 보장
- 원본 엔티티를 조회한 후 프록시를 조회하는 예시
@Test public void 영속성컨텍스트와_프록시2() { Member newMember = new Member("member1", "회원1"); em.persist(newMember); em.flush(); em.clear(); Member findMember = em.find(Member.class, "member1"); Member refMember = em.getReference(Member.class, "member1"); System.out.println("refMember Type = " + refMember.getClass()); System.out.println("findMember Type = " + findMember.getClass()); Assert.assertTrue(refMember == findMember); //성공 }
//출력 결과 refMember Type = class jpabook.advanced.Member findMember Type = class jpabook.advanced.Member
- 원본 엔티티를 조회하면 DB에서 조회했으므로 프록시를 반환할 이유가 없어 원본 반환
15.3.2 프록시 타입 비교
- 프록시는 원본 엔티티를 상속받아 만들어지므로 비교할 때는 ==이 아닌
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 }
//출력 결과 refMember Type = class jpabook.advanced.Member_$$_jvsteXXX
- 프록시는 원본 엔티티의 자식 타입이므로
instanceof
연산을 사용
- 프록시는 원본 엔티티의 자식 타입이므로
15.3.3 프록시 동등성 비교
- 엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서
equals()
를 오버라이딩으로 구현
- IDE, 외부 라이브러리를 통해 구현한
equals()
는 원본 엔티티는 문제없지만 프록시는 문제 발생
- 프록시 동등성 비교 예시
@Entity public class Member { @Id private String id; private String name; ... @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 (name != null ? !name.equals(member.name) : member.name != null) // -- 2 return false; return true; } @Override public int hashCode() { return name != null ? name.hashCode() : 0; } }
- name 필드를 비즈니스 키로 사용해서 equals() 메소드를 오버라이딩 처리
- 프록시 동등성 비교, 실행
@Test public void 프록시와_동등성비교() { Member saveMember = new Member("member1", "회원1"); em.persist(saveMember); em.flush(); em.clear(); Member newMember = new Member("member1", "회원1"); Member refMember = em.getReference(Member.class, "member1"); Assert.assertTrue(newMember.equals(refMember)); }
- 프록시로 테스트를하면 실패가 되고, 원본 엔티티를 조회해서 비교하면 성공
- 첫번째는 표시된 타입 동등성에서 문제가 발생
//this.getClass() ≠ obj.getClass()로 동등성 비교를 하기 때문 if (this.getClass() != obj.getClass()) return false; // -- 1 //== 비교가 아닌 instanceof로 변경 if(! (obj instanceof Member)) return false;
- 두번째는 equals() 메소드에서 문제가 발생
Member member = (Member) obj; //member는 프록시 if (name != null ? !name.equals(member.name) : member.name != null) // -- 2 return false;
- member.name을 보면 프록시의 멤버변수의 접근하지만 프록시는 실제 데이터가 없음 따라서 프록시의 멤버변수에 접근해도 값을 조회할 수 없는 문제 발생 (null 반환)
- 프록시의 데이터를 조회할 때는 접근자
Getter
를 사용해서 접근자를 사용하도록 수정
Member member = (Member) obj; //member는 프록시 if (name != null ? !name.equals(member.getName()) : member.getName() != null) // -- 2 return false;
- equals() 를 수정한 전체 코드
@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 (name != null ? !name.equals(member.getName()) : member.getName() != null) // -- 2 return false; return true; }
- 프록시의 타입 비교는 == 비교 대신 instanceof를 사용
- 프록시의 멤버변수에 직접 접근하면 안 되고 접근자 메소드(Getter)를 사용
15.3.4 상속관계와 프록시
- 프록시를 부모 타입으로 조회하면 문제가 발생
- 프록시 부모 타입으로 조회하는 예시
@Test public void 부모타입으로_프록시조회() { //테스트 데이터 준비 Book saveBook = new Book(); saveBook.setName("jpabook"); saveBook.setAuthor("kim"); em.persist(saveBook); em.flush(); em.clear(); //테스트 시작 Item proxyItem = em.getReference(Item.class, saveBook.getId()); System.out.println("proxyItem = " + proxyItem.getClass()); if(proxyItem instanceof Book) { System.out.println("proxyItem instanceof Book"); Book book = (Book) proxyItem; System.out.println("책 저자 = " + book.getAuthor()); } //결과 검증 Assert.assertFalse(proxyItem.getClass() == Book.class); Assert.assertFalse(proxyItem instanceof Book); Assert.assertTrue(proxyItem instanceof Item); }
//출력 결과 proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
em.getReference()
를 사용해 Item 엔티티 프록시를 조회- 실제 조회된 엔티티는 Book이므로 Book 타입을 기반으로 원본 엔티티 인스턴스 생성
em.getReference()
에서 Item 엔티티를 조회했으므로 proxyItem은 Item 기반으로 생성- 이 프록시 클래스는 원본 엔티티로 Book 엔티티를 참조
- 출력 결과, proxyItem이 Book이 아닌 Item 클래스 기반으로 만들어지는 것을 확인
- 프록시인 proxyItem은
Item$Proxy
타입이고 이 타입은 Book 타입과 관계가 없기 떄문proxyItem instanceof Book //false
- 직접 다운 캐스팅을 해도 문제가 발생, proxyItem은 Book 타입이 아닌 Item 타입을 기반으로 한
Item$Proxy
타입이므로ClassCastException
예외가 발생Book book = (Book) proxyItem; //java.lang.ClassCastException
내용 정리
- 프록시를 부모 타입으로 조회하면 부모 타입을 기반으로 프록시가 생성되는 문제 발생
instanceof
연산을 사용 못함
- 하위 타입으로 다운캐스팅 못함
- 프록시를 부모 타입으로 조회할때 주로 문제가 되는 예제 (다형성을 다루는 도메인 모델)
@Entity public class OrderItem { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ITEM_ID") private Item item; public Item getItem() { return item; } public void setItem(Item item) { this.Item = item; } ... }
다형성과 프록시 조회 실행
@Test public void 상속관계와_프록시_도메인모델() { //테스트 데이터 준비 Book book = new Book(); book.setName("jpaBook"); book.setAuthor("kim"); em.persist(book); OrderItem saveOrderItem = new OrderItem(); saveOrderItem.setItem(book); em.persist(saveOrderItem); em.flush(); em.clear(); //테스트 시작 OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId()); Item item = orderItem.getItem(); System.out.println("item = " + item.getClass()); //결과 검증 Assert.assetFalse( item.getClass() == Book.class ); Assert.assetFalse( item instanceof Book ); Assert.assetTrue( item instanceof Item ); }
//출력결과 item = class jpabook.proxy.advanced.item.Item_$$_jvstffa_3
- OrderItem과 연관된 Item을 지연로딩으로 설정했으므로 출력 결과 Item 프록시가 조회
- item instanceof Book 연선도 false를 반환
<상속관계에 발생하는 프록시 문제 해결 방법>
JPQL로 대상 직접 조회
- 가장 간단한 방법은 처음부터 자식 타입을 직접 조회해서 연산 (다형성 활용 X)
Book jpqlBook = em.createQuery ("select b from Book b where b.id=:bookId", Book.class) .setParameter("bookId"m item.getId()) .getSingleResult();
프록시 벗기기
- 하이버네이트가 제공하는 기능을 사용하면 원본 엔티티 사용이 가능
... 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()); } Assert.assetTrue(item != unProxyItem); } //하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드 public static <T> T unProxy(Object entity) { if (entity instanceof HibernateProxy) { entity = ((HibernateProxy) entity) .getHibernateLazyInitializer(); .getImplementation(); } return (T) entity; }
//출력 결과 proxyItem instanceof Book 책 저자 = shj
- 영속성 컨텍스트는 한번 프록시로 노출한 엔티티는 계속 프록시로 노출
- 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 발생
item == unProxyItem //false
- 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른곳에서 사용하지 않는 것이 중요
기능을 위한 별도의 인터페이스 제공
- 특정 기능을 제공하는 인터페이스를 사용
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 + "]"; } }
- TitleView라는 공통 인터페이스를 만들고 자식 클래스들은
getTitle()
을 각각 구현
- OrderItem에서 Item의
getTitle()
을 호출@Entity public class OrderItem { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ITEM_ID") private Item item; ... public void printItem() { System.out.println("TITLE=" + item.getTitle()); } }
OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId()); orderItem.printItem();
- Item의 구현체에 따라 각각 다른 getTitle() 호출
- 각각 클래스가 자신의 맞는 기능을 구현하는 것은 다형성을 활용하는 좋은 방법
- 클라이언트 입장에서 대상 객체가 프록시인지 아닌지 고민하지 않아도 되는 장점
비지터 패턴 사용
- Item이
accepct(visotr)
를 사용해 Visitor을 받아들이고 실제 로직은 Visitor가 처리
- Visitor 정의와 구현
- 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) {...} }
- Visitor 인터페이스
- 대상 클래스 작성
- Visitor 대상 클래스
@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); } }
- 파라미터로 넘어온 Visitor의 visit(this)를 호출해 자신(this)을 파라미터로 넘겨 처리
- 비지터 패턴 실행
@Test public void 상속관계와_프록시_VisitorPattern() { ... OrderItem orderItem = em.find(OrderItem.class, orderItemId); Item item = orderItem.getItem(); //PrintVisitor item.accept(new PrintVisitor()); }
//출력 결과 book.class = class jpabook.advanced.item.Book [PrintVisitor] [제목:jpabook 저자:kim]
- item.accept()를 호출하면서 파라미터 PrintVisitor를 전달
- item은 프록시이므로 먼저 프록시가
accept()
를 받고 원본 엔티티의accept()
를 실행
- 원본 엔티티는 코드를 실행해 자신(this)을 visitor에 파라미터로 전달
public void accept(Visitor visitor) { visitor.visit(this); //this는 프록시가 아닌 원본 }
- visitor가 PrintVisitor타입이므로
PrintVisitor.visit(this)
가 실행- this가 Book 타입이므로
visit(Book book)
가 실행
- this가 Book 타입이므로
- Visitor 대상 클래스
- 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 원본 엔티티에 접근 가능
- instanceof나 타입캐스팅 없이 코드를 구현할수 있는 장점
비지터 패턴과 확장성과 패턴 정리
- 비지터 패턴과 확장성
- TitleVisitor를 사용하는 예제
//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]
- 비지터 패턴은 새로운 기능이 필요할 때 Visitor만 추가하면 되므로 기존 코드의 구조를 변경하지 않고 기능을 추가할 수 있는 장점
- TitleVisitor를 사용하는 예제
- 비지터 패턴 정리
- 장점
- 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근 가능
- instanceof와 타입캐스팅 없이 코드를 구현
- 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가
- 단점
- 너무 복잡하고 더블 디스패치를 사용하기 떄문에 이해하기 어려움
- 객체 구조가 변경되면 모든 Visitor를 수정
- 장점
'개발서적 > 자바 ORM 표준 JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 16.1 트랜잭션과 락 (0) | 2021.10.25 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 15.4 성능 최적화 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.2 엔티티 비교 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.1 예외 처리 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 14.4 엔티티 그래프 (0) | 2021.10.25 |