티스토리 뷰
| 엔티티 비교
--
|| 영속성 컨텍스트가 같을 때 엔티티 비교
* 동일성(identical) : == 비교가 같다.
* 동등성(equinalent) : equals() 비교가 같다.
* 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.
|| 영속성 컨텍스트가 다를 때 엔티티 비교
* 동일성(identical) : == 비교 실패.
* 동등성(equinalent) : equals() 비교 만족. 단 equals() 구현 필요.(보통 비즈니스 키로 구현)
ㄴ엔티티 비교 시 비즈니스 키를 활용한 동등성 비교를 권장.
* 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.
| 프록시 심화
--
프록시는 원본 엔티티를 상속받아서 만들어짐.
|| 영속성 컨텍스트와 프록시
1.
em.getReference(member) 로 프록시 조회 후
em.find(member)로 조회했을 경우
=> 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환
2.
em.find(member)로 원본 엔티티 조회한 후
em.getReference(member)로 프록시 조회를 할 경우
=> 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회했으므로 프록시가 아닌 원본을 반환
|| 프록시 타입 비교
* 프록시는 원본 엔티티의 자식 타입이므로 instanceof 연산을 사용
if (refMember instanceof Member)
|| 프록시 동등성 비교
equals() 메소드를 오버라이딩 했을 경우
1. 프록시 타입 비교는 == 비교 대신 instanceof를 사용
if(!(obj instanceof Member)) return false;
2. 프록시의 멤버변수에 직접 접근하면 안 되고 대신 접근자 메소드를 사용
if(name != null ? !name.equals(member.getName()) : member.getName() != null)
return false;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Override public boolean equals(Object obj) { if(this == obj) return true; if(obj == null) return false; // 프록시와 equals() 비교 if(!(obj instanceof Member)) return false; Member member = (Member) obj; // 프록시의 데이터 조회 if(name != null ? !name.equals(member.getName()) : member.getName() != null) return false; return true; } | cs |
|| 상속관계와 프록시
* 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성된는 문제가 발생
- 다형성을 다루는 도메일 모델에서 자주 나타나는 현상
1 2 | // Book 타입을 원했지만 Item$Proxy 타입. Item proxyItem = em.getReference(Item.class, saveBook.getId()); | cs |
- instanceof 연산 불가
if(proxyItem instanceof Book)
- 하위 타입으로 다운캐시팅 불가
Book book = (Book) proxyItem;
상속관계에서 발생하는 프록시 문제를 해결하기 위한 방법
||| 기능을 위한 별도의 인터페이스 제공
- 인터페이스를 제공하고 각각의 클래스가 자신에 맞는 기능을 구현 (다형성 활용에 좋은 방법)
- 프록시의 특징으로 프록시의 대상이 되는 타입에 인터페이스를 적용해야 함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public interface TitleVie { 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 ig; private String name; private int price; private int stockQuantity; // Getter, Sstter } @Entity @DiscriminatorValue("B") public class Book extends Item { private String author; private String isbn; // Getter, Setter @Override public String getTitle() { return "this is book"; } } @Entity @DiscriminatorValue("M") public class Movie extends Item { private String director; private String author; // Getter, Setter @Override public String getTitle() { return "this is Movie"; } } | cs |
||| 비지터 패턴 사용
- 비지터 패턴
알고리즘을 객체 구조에서 분리시키는 디자인 패턴이다. 이렇게 분리를 하면 구조를 수정하지 않고도 실질적으로 새로운 동작을 기존의 객체 구조에 추가할 수 있게 된다. 개방-폐쇄 원칙을 적용하는 방법의 하나이다. (https://ko.wikipedia.org/wiki/비지터_패턴)
출처 : https://ehclub.co.kr/2245
||| Visitor 정의와 구현
c. Visitor Interface
1 2 3 4 5 6 | public interface Visitor { void visit(Book book); void visit(Album album); void visit(Movie movie); } | cs |
c. Visitor 구현
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /** 대상 클래스의 내용 출력 */ public class PrintVisitor implements Visitor { @Override public void visit(Book book) { // 넘어오는 book은 Proxy가 아닌 원본 엔티티 System.out.println(book.getClass()); System.out.println(book.getName() + " " + book.getAuthor()); } @Override public void visit(Album album) { ... } @Override public void visit(Movie movie) { ... } } /** 대상 클래스의 제목을 보관 */ 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) { ... } } | cs |
||| 대상 클래스 작성
c. Visitor 대상 클래스
- Item에 Visitor를 받아들일 수 있도록 accept(visitor) 메소드 추가
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "DTYPE") public abstract class Item { @Id @GeneratedValue @Column(name ="ITEM_ID") private Long ig; private String name; private int price; private int stockQuantity; // Getter, Sstter 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 { private String director; private String author; // Getter, Setter @Override public void accept(Visitor visitor) { visitor.visit(this); } } | cs |
||| Visitor 패턴 실행
c. Visitor 사용
- item은 프록시이므로 먼저 프록시가 accept() 메소드를 받고 원본 엔티티의 accept()를 실행
- 원본 엔티티는 자신(this)을 visitor에 파라미터로 넘겨줌
1 2 3 4 5 6 7 8 9 | @Test public void VisitorPattern() { // ... OrderItem orderitem = em.find(OrderItem.class, orderItemId); Item item = orderItem.getItem(); item.accept(new PrintVisitor()); } | cs |
c. TitleVisitor 사용
- 비지터 패턴은 새로운 기능이 필요할 때 Visitor만 추가하면 됨
1 2 3 4 | TitleVisitor titleVisitor = new TitleVisitor(); item.accept(titleVisitor); String title = titleVisitor.getTitle(); | cs |
||| Visitor 패턴의 장단점
> 장점
- 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근
- instanceof와 타입캐스팅 없이 코드를 구현
- 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작 추가
> 단점
- 너무 복잡하고 이해하기 어려움
- 객체 구조가 변경되면 모든 Visitor를 수정해야 함
| 성능 최적화
--
|| N+1 문제
- N+1 문제는 즉시 로딩과 지연 로딩일 때 모두 발생할 수 있음
- N+1 문제를 피할 수 있는 방법
||| 페치 조인 사용
- N+1 문제를 해결하는 가장 일반적인 방법
- 페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회
- 일대다 조인일 경우 DISTINCT를 사용해서 중복을 제거
"select m from Member m join fetch m.orders"
SELECT M.*, O.*
FROM MEMBER M
INNER JOIN ORDERS O ON M.ID = O.MEMBER_ID
||| 하이버네이트 @BatchSize
- BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해서 조회
(org.hibernate.annotations.BatchSize)
* hibernate.default_batch_fetch_size 속성을 사용하면 애플리케이션 전체에 기본으로 @BatchSize 적용
1 | <property name="hibernate.default_batch_fetch_size" value="5" /> | cs |
- 즉시 로딩을 설정하면 조회 시점에 10건의 데이터를 모두 조회해야 하므로 아래 SQL이 두 번 실행
- 지연 로딩으로 설정하면 지연 로딩된 엔티티를 최초 사용하는 시점에 아래 SQL을 실행해서 5건의 데이터를 미리 로딩
6번째 데이터를 사용하려면 아래 SQL을 추가로 실행
SELECT *
FROM ORDERS
WHERE MEMNER_ID IN (
?, ?, ?, ?, ?
}
1 2 3 4 5 6 7 8 9 10 11 | @Entity public class Member { // .. @org.hibernate.annotations.BatchSize(size = 5) @OneToMany(mappedBy = "member", fetch = FetchType.EAGER) privte List<Order> orders = new ArrayList<Order>(); //.. } | cs |
||| 하이버네이트 @Fetch(FetchMode.SUBSELECT)
- Fetch 어노테이션에 FetchMode를 SUBSELECT로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용
(org.hibernate.annotations.Fetch)
- 즉시 로딩으로 설정하면 조회 시점에,
지연 로딩으로 설정하면 지연 로딩된 엔티티를 사용하는 시점에 SQL 실행
1 2 3 4 5 6 7 8 | @Entity public class Member { // .. @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT) @OneToMany(mappedBy = "member", fetch = FetchType.EAGER) privte List<Order> orders = new ArrayList<Order>(); //.. } | cs |
||| 권장 방법
- 즉시 로딩을 사용하지 않고 지연 로딩만 사용하는 것을 권장
- 즉시 로딩을 사용할 경우의 문제점
1. N+1 문제
2. 비즈니스 로직에 필요하지 않은 엔티티를 로딩하는 상황이 자주 발생
3. 성능 최적화의 어려움
4. 즉시 로딩이 연속으로 발생하여 예상치 못한 SQL이 실행될 수 있음
- 모두 지연 로딩으로 설정하고 성능 최적화가 필요한 곳에는 JPQL 페치 조인 사용
|| 읽기 전용 쿼리의 성능 최적화
ㅇ 변경 감지가 필요 없을 경우 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있음
- 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있음
> 스칼라 타입으로 조회 (메모리 최적화)
- 스칼라 타입은 영속성 컨텍스트가 결과를 관리하지 않음
select o.id, o.name, o.price
from Order o
> 읽기 전용 쿼리 힌트 사용 (메모리 최적화)
- org.hibernate.readOnly
- 엔티티를 읽기 전용으로 조회
- 영속성 컨텍스트는 스냅샷을 보관하지 않음 -> 메모리 사용량 최적화
- 단, 스냅샷이 없으므로 엔티티를 수정해도 DB 반영 X
1 2 | TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class); query.setHint("org.hibernate.readOnly", true); | cs |
> 읽기 전용 트랜잭션 사용 (플러시 호출을 막아 속도 최적화)
- @Transactional(readOnly = true)
- 하이버네이트 세션의 플러시 모드르 MANUAL로 설정
- 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않음
- 플러시할 때 일어나는 스냅샷 비교와 같은 무거운 로직들을 수행하지 않으므로 성능 향상
- 평소 트랜잭션과 동작이 같지만, 단지 영속성 컨텍스트를 플러시하지 않음
> 트랜잭션 밖에서 읽기 (플러시 호출을 막아 속도 최적화)
- 트랜잭션 없이 엔티티를 조회
- 조회가 목적일 때만 사용
- @Transactional(propagation = Propagation.NOT_SUPPORTED)
- 트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회 성능 향상
* 읽기 전용 트랜잭션(or 트랜잭션 밖에서 읽기)과 읽기 전용 쿼리 힌트(or 스칼라 타입으로 조회)를
동시에 사용하는 것이 가장 효과적
1 2 3 4 5 6 7 8 9 | // 읽기 전용 트랜잭션 (플러시 작동을 막아 성능 향상) @Transactional(readOnly = true) public List<DataEntity> findDatas() { return em.createQuery("select d from DataEntity d", DataEntity.class) // 읽기 전용 쿼리 힌트 (엔티티 읽기 전용으로 메모리 절약) .setHint("org.hibernate.readOnly", true) .getResultList(); } | cs |
출처 : 자바 ORM 표준 JPA 프로그래밍
'Books' 카테고리의 다른 글
[클린 코드: Clean Code] 3. 함수(Function) (0) | 2021.01.11 |
---|---|
[클린 코드: Clean Code] 2. 의미 있는 이름(Meaningful name) (0) | 2021.01.10 |
[JPA] 엔티티 그래프 (Entity Graph) (0) | 2020.12.29 |
[JPA] 영속성(persistence) 관리 (OSIV) (0) | 2020.12.28 |
[Spring Data JPA] Make Web Application (3) (0) | 2020.12.28 |