티스토리 뷰

반응형


| 엔티티 비교


--


|| 영속성 컨텍스트가 같을 때 엔티티 비교



* 동일성(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 == nullreturn 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 프로그래밍

반응형
댓글
최근에 올라온 글
최근에 달린 댓글
링크
Total
Today
Yesterday