본문 바로가기

Back-End/JPA

JPA 15장. 고급 주제와 성능 최적화

성능 최적화 1 - 읽기 전용 쿼리의 성능 최적화

Entity가 영속성 컨텍스트에 관리되면 다양한 혜택을 얻을 수 있다. 하지만 dirty check를 위해 snapshot 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 대량의 데이터를 조회만 할거기 때문에 읽기 전용으로 메모리 사용량을 최적화할 수 있다.

 

1) 읽기 전용 트랜잭션 사용 - 속도 최적화

@Transactional(readOnly=true)

해당 옵션을 주면 Spring Framework가 Hibernate Session의 Flush 모드를 Manual로 설정하여, 강제로 flush()를 하지 않는 한 flush()가 일어나지 않는다.

  • 트랜잭션이 커밋되어도 플러시되지 않음
  • flush() 할 때 일어나는 스냅샷 비교와 같은 무거운 로직을 수행하지 않음 -> 성능 향상
  • 트랜잭션 시작, 로직수행, 커밋은 똑같이 동작함

2) 읽기 전용 쿼리 힌트 사용 - 메모리 최적화

TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);
query.setHint("org.hibernate.readOnly", true);
  • 엔티티를 읽기 전용으로 조회하므로 영속성 컨텍스트는 스냅샷을 보관하지 않음
  • 엔티티를 수정해도 데이터베이스에 반영되지 않음

3) 스칼라 타입으로 조회

스칼라 타입은 영속성 컨텍스트가 관리를 하지 않는다.

 

아래와 같이 사용하는게 좋다고 한다.

@Transactional(readOnly = true ) // 플래시 작동하지 않아서 성능 향상
public List<DataEntity> findDatas() {
	return em.createQuery("...")
    		.setHint("org.hibernate.readOnly", true);
            .getResultList();	// 엔티티 읽기전용이라 메모리 절약
}

 

성능 최적화 2 - 배치처리

수백만 건의 데이터를 배치 처리해야 하는 상황이라면?

엔티티를 계속 조회하다보면 영속성 컨텍스트에 아주 많은 엔티티가 쌓여서 메모리 부족 오류가 발생할 것이다. 따라서 이런 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화 해줘야 한다.

EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

for(int i=0; i<10000; i++){
	Product product = new Product(i,1000);
    em.persist(product);
    
    if(i%100 == 0){
    	em.flush(); em.clear();
    }
}

tx.commit();
em.close();
  • 다른 방법 - JPA 페이징 배치 처리
  • 다른 방법 - Hibernate scroll 사용
  • 다른 방법 - Hibernate 무상태 세션 사용

성능 최적화 3 - SQL 쿼리 힌트 사용

JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않는다. SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다.

Session session = em.unwrap(Session.class); // hibernate 직접 사용

List<Member> list = session.createQuery("select m from Member m")
						.addQueryHint("FULL (MEMBER)")
                        .list();

 

성능 최적화 4 - 트랜잭션을 지원하는 쓰기 지연과 성능 최적화

트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 덕분에 데이터베이스 테이블 row에 lock이 걸리는 시간이 최소화된다. 

트랜잭션을 커밋해서 영속성 컨테스트가 플러시하기 전 까지는 데이터베이스에 데이터를 등록,수정,삭제를 하지 않는다. 그러므로 데이터베이스 로우에 락을 걸지 않는다.

update(memberA); // UPDATE SQL A ...
비즈니스로직A(); // UPDATE SQL...
비즈니스로직B(); // INSERT SQL....
commit();
  • JPA 없이 SQL을 직접 다룬다면?
    1. UPDATE SQL문이 실행되면서 데이터베이스에 락을 건다.
    2. 이 락은 로직 A,B가 모두 수행되고 commit이 되면 풀린다.
    3. 일반적인 Read committed 격리 수준에서는 데이터베이스에 현재 수정중인 데이터를 수정하려는 다른 트랜잭션은 락이 풀릴 때까지 대기한다.
  • JPA와 함께한다면?
    • commit() -> flush()를 해야 데이터베이스에 수정쿼리를 보낸다.
    • 위에 3가지 함수가 동시에 날라가서 락이 잡히기 때문에 락이 걸리는 시간이 최소화된다!

사용자가 증가하면 애플리케이션 서버를 더 증설하면 된다. 하지만 데이터베이스 락은 애플리케이션 서버 증설만으로는 해결할 수 없다. 오히려 서버가 많아지면 트랜잭션이 많아져서 더 많은 데이터베이스 락이 걸리게 된다. JPA의 쓰기지연 기능 덕분에 락이 걸리는 시간이 최소화돼서 동시에 더 많은 트랜잭션을 처리할 수 있게 된다!

 

성능 최적화 5 - N+1 문제 해결

아마 개발자가 가장 많이 직면하는 문제가 아닐까 싶다. 

 

 

엔티티 비교하기

1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다. 영속성 컨텍스트를 더 정확히 이해하기 위해서는 1차캐시의 가장 큰 장점인 애플리케이션 수준의 반복 가능한 읽기를 이해해야 한다. 

 

Member member1=em.find(Member.class,"1L");
Member member2=em.find(Member.class,"1L");

assertTrue(member1==member2); // 같은 인스턴스

같은 영속성 컨텍스트에서 엔티티를 조회하면 위의 코드와 같이 항상 같은 엔티티 인스턴스를 반환한다. 

 

영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 조건을 만족해야 한다.

  1. ==
  2. equals()
  3. @Id인 데이터베이스 식별자가 같다
테스트 클래스에서 @Transactional을 선언하면 테스트가 끝날 때 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백한다. 그렇기 때문에 영속성 컨텍스트를 플래스하지 않는다. 결국, 플러시 시점에 어떤 SQL이 실행되었는지 콘솔 로그에 남지 않는다. 콘솔로 보고 싶다면 마지막에 em.flush() 강제 호출이 필요하다.

 

영속성 컨텍스트가 다를 때 엔티티 비교

public class MemberServiceTest(){

  @Test
  public void main {
  Member member = new Member("kim");

  // 시작되면서 영속성컨텍스트1이 생성됨
  Long saveId = memberservice.join(member); 
  // 이 때 트랜잭션과 영속성 컨텍스트가 종료됨 -> member : detached

  // 시작되면서 영속성 컨텍스트2가 생성됨 
  Member findMember = memberRepository.findOne(saveId);
  
  assertFalse(member == findMember); // 참조값 비교
    }
 }
  @Transactional
  public class MemberService {
    public Long join(Member member) {
      ...
      memberRepository.save(member);
      return memger.getId();
      }
  }

  @Repository
  @Transactional
  public class MemberRepository {

   public void save(Member member){
     em.persist(member);
   }

   public Member findOne(Long id){
     return em.find(Member.class, id);
   }
 }