본문 바로가기
  • 실행력이 모든걸 결정한다
Spring 사전 준비/JPA Hibernate

[JPA] 성능 최적화

by 김코더 김주역 2022. 8. 27.
반응형

1. N+1 문제

1) N+1 문제란?

- 처음 실행한 결과 수만큼 추가적인 SQL이 실행되어 성능에 안좋은 영향을 미치는 문제를 뜻한다.

- 즉시 로딩과 지연 로딩일 때 모두 발생할 수 있다.

 

 

2) N+1 문제 해결 방법

(1) 페치 조인

- SQL 조인을 사용해서 연관된 엔티티를 함께 조회하는 방법이다.

- 사용 방법은 https://kimcoder.tistory.com/493의 [2-7) JOIN FETCH]를 참고하자.

 

(2) @BatchSize

- org.hibernate.annotations.BatchSize 어노테이션을 적용해서 배치를 사용하는 방법이다.

- 연관된 엔티티를 조회할 때 지정한 크기만큼 SQL의 IN절을 사용해서 조회한다. 만약 조회 건수가 100이고 배치 크기가 20이라면 총 5번의 SQL문이 실행될 것이다.

@Entity
public class Team {

    ...
    
    @org.hibernate.annotations.BatchSize(size=20)
    @OneToMany(mappedBy="team", fetch=FetchType.EAGER)
    private List<Member> members = new ArrayList<Member>(); 
}

- 아래 속성을 추가하면 애플리케이션 전체에 기본적으로 @BatchSize를 적용할 수도 있다.

<property name="hibernate.default_batch_fetch_size" value="20" />

 

(3) @Fetch(FetchMode.SUBSELECT)

- org.hibernate.annotations.Fetch 어노테이션에 FetchMode.SUBSELET를 적용하는 방법이다.

- 연관된 엔티티를 조회할 때 SQL의 서브 쿼리를 사용해서 N+1문제를 해결한다.

@Entity
public class Team {

    ...
    
    @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy="team", fetch=FetchType.EAGER)
    private List<Member> members = new ArrayList<Member>(); 
}

 

 

3) N+1 문제 추천 전략

- 즉시 로딩은 성능 최적화가 어렵기 때문에 가급적 지연 로딩을 사용한다. 기본값이 즉시 로딩(FetchType.EAGER)인 @OneToOne과 @ManyToOne은 FetchType.LAZY로 변경해서 사용하도록 하자.

- 성능 최적화가 필요한 곳에는 JPQL 페치 조인을 사용한다.

 

 

 

2. 읽기 전용으로 조회

1) 스냅샷 인스턴스와 메모리 최적화

- 영속성 컨텍스트는 엔티티의 변경 감지를 위해 엔티티에 대한 스냅샷 인스턴스를 보관하지만 그만큼 더 많은 메모리를 사용한다. 그래서 엔티티를 조회만 하고 수정은 하지 않을 것이라면 읽기 전용으로 조회하여 메모리 사용량을 최적화 하는 것이 좋다.

 

 

2) 읽기 전용 설정 방법

(1) 스칼라 타입으로 조회

- 엔티티가 아닌 스칼라 타입은 영속성 컨텍스트가 관리하지 않는다는 특징을 이용한다.

 

(2) 읽기 전용 쿼리 힌트 사용

- Hibernate 전용 힌트인 org.hibernate.readOnly를 사용한다.

TypedQuery<Member> query = em.createQuery("SELECT m from Member m", Member.class);
query.setHint("org.hibernate.readOnly", true);

 

(3) 읽기 전용 트랜잭션 사용

- @Transactional.readOnly 속성을 true로 설정하면 트랜잭션이 커밋되어도 영속성 컨텍스트를 flush하지 않는다. 물론, EntityManager.flush()를 호출해서 강제로 flush할 수는 있지만 좋지 않은 과정이다.

@Transactional(readOnly=true)

※ 내부적으로 org.hibernate.Session의 플러시 모드가 MANUAL로 설정된다. MANUAL 모드는 강제로 플러시를 호출하지 않는 한 절대 플러시가 발생하지 않는다. 참고로, org.hibernate.Session은 JPA EntityManager를 hibernate로 구현한 구현체로, EntityManager.unwrap() 메소드로 구할 수 있다.

Session session = em.unwrap(Session.class); // EntityManager로부터 org.hibernate.Session 얻기

 

(4) 트랜잭션 없이 엔티티 조회

- 트랜잭션을 아예 사용하지 않도록 해서 커밋 자체가 일어날 일이 없도록 하는 방법이다.

@Transactional(propagation=Propagation.NOT_SUPPORTED)

 

 

3) 읽기 전용 추천 전략

- 메모리 최적화 : 스칼라 타입으로 조회 또는 읽기 전용 쿼리 힌트 사용

- 속도 최적화 : 읽기 전용 트랜잭션 사용 또는 트랜잭션 없이 엔티티 조회

※ 메모리 최적화와 속도 최적화를 동시에 수행하자.

 

 

 

3. 배치 처리

1) 메모리 부족 오류와 배치 처리

- 무수히 많은 엔티티를 조회하다보면 영속성 컨텍스트에 엔티티들이 점점 쌓이면서 메모리 부족 오류가 발생할 수 있다. 그래서 적절한 단위로 배치 처리를 할 때마다 영속성 컨텍스트를 초기화해야 한다.

 

 

2) 배치 처리 방법

(1) 등록 배치

for(int i=0;i<10000;i++) {
    Member member = new Member("member"+i);
    em.persist(member);
    if(i%100==0) { // 100건 마다 flush, clear
        em.flush();
        em.clear();
    }
}

 

(2) 페이징 배치 처리

int pageSize = 100;
for(int i=0;i<50;i++) {
    
    // 페이징을 이용하여 100건 단위로 엔티티를 조회 
    List<Member> memberList = em.createQuery("SELECT m FROM Member m", Member.class)
        .setFirstResult(pageSize*i)
        .setMaxResults(pageSize)
        .getResultList();
    
    // 비즈니스 로직 수행
    ...
    
    em.flush();
    em.clear();
}

 

(3) Hibernate에서 제공하는 스크롤 사용

- 스크롤은 hibernate 전용 기능이기 때문에 org.hibernate.Session을 얻어서 사용해야 한다.

Session session = em.unwrap(Session.class); // EntityManager로부터 org.hibernate.Session 얻기
ScrollableResults scroll = session.createQuery("SELECT m FROM Member m")
    .setCacheMode(CacheMode.IGNORE)
    .scroll(ScrollMode.FORWARD_ONLY);

int cnt=0;

while(scroll.next()) {
    Member member = (Member) scroll.get(0);
    
    // 비즈니스 로직 수행
    ...
    
    cnt++;
    if(cnt%100==0) {
        session.flush();
        session.clear();
    }
}
session.close();

 

(4) StatelessSession

- Hibernate에서 제공하는 StatelessSession은 영속성 컨텍스트가 없고 2차 캐시도 사용하지 않는 세션이다. 엔티티를 수정하려면 StatelessSession.update() 메소드를 사용해야 한다.

SessionFactory sessionFactory = emf.unwrap(SessionFactory.class); // EntityManagerFactory emf
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = session.createQuery("SELECT m FROM Member m").scroll();

while(scroll.next()){
    Member m = (Member) scroll.get(0);
    
    // 비즈니스 로직
    ...
    
    session.update(m);
}
tx.commit();
session.close();

 

 

 

4. SQL 힌트 사용

- JPA는 DB SQL 힌트 기능을 제공하지 않지만, Hibernate에서는 SQL 힌트를 사용할 수 있다.

- 현재 Hibernate 버전에서는 오라클 방언에만 힌트가 적용되어 있다. 오라클 DB를 사용하는 경우에는 Query의 addQueryHint() 메소드를 이용해서 SQL 힌트를 추가하면 된다.

- 다른 DB에서 SQL 힌트를 사용하려면 org.hibernate.dialect.Dialect의 getQueryHintString() 메소드를 오버라이딩해서 기능을 구현해야 한다.

 

 

 

5. 트랜잭션을 지원하는 쓰기 지연

1) JDBC 배치

- 네트워크 호출 한 번은 단순한 메소드를 수만 번 호출하는 것보다 더 큰 비용이 발생한다. JPA에서는 SQL 배치 기능을 사용하여 SQL을 모아서 DB에 한 번에 보낼 수 있다.

- Hibernate에서 등록, 수정, 삭제에 대한 SQL 배치를 사용하려면 다음과 같이 설정하면 된다.

<property name="hibernate.jdbc.batch_size" value="100"/>

- SQL 배치는 같은 형식의 SQL일 때만 유효하기 때문에 중간에 다른 처리가 들어가면 SQL 배치를 다시 시작한다.

 

 

2) Lock 시간 최소화

- 트랜잭션 격리 수준에 따라 다르지만, 대부분은 DB에 수정 중인 데이터를 수정하려는 다른 트랜잭션은 락이 풀릴 때까지 대기한다.

- JPA의 쓰기 지연 기능을 사용하면 DB 테이블의 로우에 락이 걸리는 시간을 최소화할 수 있다. 쓰기 지연 기능을 사용하면 SQL 쿼리를 보내고 바로 트랜잭션을 커밋하기 때문에 그 사이에 락이 걸리는 시간이 매우 적어지는 것이다.

 

 

● 참고자료 : 자바 ORM 표준 JPA 프로그래밍

 

반응형

'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글

[JPA] 2차 캐시  (0) 2022.08.30
[JPA] 트랜잭션과 Lock  (0) 2022.08.29
[JPA] JPA 예외 처리  (0) 2022.08.25
[JPA] 엔티티 그래프  (0) 2022.08.25
[JPA] JPA 리스너  (0) 2022.08.24

댓글