[JPA] 성능 최적화

Bam·2025년 6월 3일
0

Spring

목록 보기
71/73
post-thumbnail

이번에는 JPA의 성능 최적화 문제가 발생하는 대표적인 상황들과 그 해결 방법들을 다뤄보려고 합니다.

N+1 문제

N+1 문제는 가장 대표적이면서도 치명적인 JPA의 성능 문제입니다.

이 부분은 내용이 많기 때문에 별개의 포스트에서 다뤘었으니 해당 포스트를 참조해주세요.


읽기 전용 쿼리

주문 목록과 같은 조회 기능은 엔티티들을 딱 한 번 읽어서 화면 그려주면되고 데이터 수정이나 추가적인 조회가 필요하지 않습니다. 그래서 이런 경우 읽기 전용 쿼리를 만들어서 조회하면 메모리 사용량을 최적화할 수 있습니다.

다음 JPQL 조회 쿼리를 세 가지 방법으로 최적화해보겠습니다.

SELECT m FROM Member m
  • 스칼라 타입 조회
    방금 처럼 엔티티 m으로 조회하는 대신 m.name, m.address와 같은 스칼라 타입으로 필드를 조회하는 방법입니다.
    스칼라 타입은 영속성 컨텍스트가 결과를 관리하지 않기 때문에 영속성 컨텍스트 기능이 적용되지 않아서 메모리 사용량을 줄일 수 있게 됩니다.
SELECT m.id, m.name, m.address FROM Member m
  • 읽기 전용 쿼리 힌트
    하이버네이트 구현체 기준으로 readOnly 힌트를 사용하면 엔티티를 읽기 전용으로 조회하게 됩니다.
    마찬가지로 읽기 전용 조회는 영속성 컨텍스트가 스냅샷을 보관하지 않게 되어 메모리 사용량을 줄일 수 있습니다.
Query query = em.createQuery("SELECT m FROM Member m", Member.class);
query.setHint("org.Hibernate.readOnly", true);
  • 읽기 전용 트랜잭션
    스프링 프레임워크에서 트랜잭션을 읽기 전용으로 설정하는 방법입니다.
@Transactional(readOnly = true)

읽기 전용 트랜잭션은 명시적으로 flush를 호출하지 않는 이상 플러시 동작을 수행하지 않습니다. 따라서 쓰기, 갱신, 삭제 동작이 동작하지 않으면서 플러시 과정에서 발생하는 로직들을 수행하지 않게되어 조회 속도가 향상됩니다.

  • 트랜잭션 밖에서 읽기
    JPA에서 데이터를 변조하기 위해서는 트랜잭션 내에서 데이터를 읽어야하므로 트랜잭션 밖에서 읽게되면 읽기 동작만을 수행하게 됩니다. 마찬가지로 플러시가 발생하지 않아서 조회 속도가 향상됩니다.
@Transactional(propagation = Propagation = Propagation.NOT_SURPPORTED)

정리

메모리 최적화는 스칼라 타입 조회 or 하이버네이트의 읽기 전용 쿼리 힌트를 사용하고 속도 최적화(플러시 막기)는 읽기 전용 트랜잭션 or 트랜잭션 밖에서 읽기를 수행하면 됩니다.

(보통 하이버네이트 구현체를 많이 사용하므로) 읽기 전용 트랜잭션 설정과 읽기 전용 쿼리 힌트를 동시에 사용하면 효과적으로 메모리 및 조회 속도 최적화를 할 수 있습니다.

@Transactional(readOnly = true)
public List<Member> findAllMember() {
	
    return em.createQuery("SELECT m FROM Member m", Member.class)
    			.setHint("org.Hibernate.readOnly", true)
                .getReusltList();
}

Spring Boot 환경(Spring Data JPA)에서는 다음과 같이 리포지토리에서 @QueryHint를 사용합니다.

@Transactional(readOnly = true)
public List<Member> findAllMember() {
	
    return memberRepository.findAll();
}

public interface MemberRepository extends JpaRepository<Member, Long> {

	@QueryHints(value = {
    	 @QueryHint(name = org.hibernate.annotations.QueryHints.READ_ONLY, value = "true")
    })
    @Query("SELECT m FROM Member m")	//이 JPQL 쿼리는 생략 가능
    List<Member> findAll();
}

배치 처리 Batch Processing

무수한 데이터를 배치 처리하게 된다면 영속성 컨텍스트에 엄청난 데이터들이 쌓여서 메모리 부족 현상을 불러일으킬 수 있습니다.

따라서 적절한 단위를 지정해서 영속성 컨텍스트를 배치 처리 (Batch Processing)해야합니다.

저장 배치 처리

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

위 코드는 총 1만건의 멤버를 등록하는데, 100건마다 플러시 및 영속성 컨텍스트 clear를 수행하는 배치 처리 코드입니다.

수정 배치 처리

수정 배치 처리는 저장처럼 100건 씩 올려두고 수정할 수 없습니다. 그래서 JPA의 페이징 배치 처리 기능을 통해서 데이터를 조회하고 수정합니다.

EntityManager em = entityManagerFactory.createEntityManagerFactory();
EntityTransaction tx = em.getTransaction();

tx.begin();

for (int i = 0; i < 100; i++) {
	List<Member> members = em.createQuert(취득 쿼리 생략)
    	.setFirstResult(i * 100)	//100은 한 페이지에 표시할 데이터 수
        .setMaxResult(100)
        .getResultList();
        
    //수정을 위한 비지니스 로직
    
    em.flush();
    em.clear();
}

tx.commit();
em.close();

위 코드를 보시면 페이징을 통해 한 번에 100건 씩 총 100번에 나눠서 수정 비지니스 로직을 수행합니다. 그리고 한 번의 페이징 처리가 끝나면 플러시 및 영속성 컨텍스트 초기화를 수행합니다.

스프링 부트에서는 페이징을 Pageable을 통해 제공하고 있으므로 해당 기능을 사용하면 됩니다.

0개의 댓글