본문 바로가기
JPA

JPA - 읽기 전용으로 데이터를 조회하여 성능 향상(메모리, 속도)

by khds 2021. 9. 18.

 

들어가기

 

JPA를 사용하면서 영속성 컨텍스트를 통해 1차 캐시 및 쓰기 지연 등 여러 가지 이점을 얻을 수 있었다.

그런데 조회 기능만을 사용하는 상황에서는 단순히 읽기 전용 기능으로 데이터를 조회만 가능하게 하고 데이터의 수정을 불가능하게 할 수 있다고 한다.

이런 방식을 적용하면 두 가지 장점을 얻을 수 있다.

바로 메모리와 속도의 최적화이다.

메모리 사용량을 최적화하는 읽기 전용 기능과 속도를 최적화하는 읽기 전용 기능은 각각 다른 방식으로 구현된다.

이 글은 메모리와 속도를 최적화하기 위한 읽기 전용 기능에 대해 작성한 글이다. 영속성 컨텍스트에 대한 내용이 많이 나오는데, 이에 대한 것은 https://khdscor.tistory.com/110를 참고하길 바란다.

 

메모리 최적화

 

먼저 메모리 사용량을 최적화하는 방식부터 살펴보겠다. 처음에 엔티티를 가져올 때 엔티티의 최초 상태를 복사해서 저장해 두는데 이것을 스냅샷이라고 한다. 메모리 사용량을 최적화하는 방식은 스냅샷을 저장하지 않도록 하여 메모리를 최적화하는 것이다. 물론 스냅샷만 저장하지 않는 것이지, 1차 캐시에는 그대로 저장한다. 그러니 fetch join처럼 1차 캐시가 필요한 경우에도 읽기 전용을 사용해도 상관없다.

 

1. 스칼라 타입으로 조회

 

영속성 컨텍스트는 1차 캐시에 엔티티를 저장한다. 하지만 엔티티가 아니라 스칼라 데이터를 조회하면 영속성 컨텍스트에 저장되지 않는다. 1차 캐시에 저장되지가 않으므로 스냅샷도 보관하지 않는다.

이렇듯 엔티티가 아닌 스칼라 타입으로 필요한 필요한 필드들을 조회한다면 스칼라 타입은 영속성 컨텍스트가 결과를 관리하지 않으므로 엔티티를 보관해야 할 메모리를 최적화할 수 있다. 

 

2. 읽기 전용 쿼리 힌트 사용

 

하이버네이트 전용 쿼리 힌트인 org.hibernate.readOnly를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다. 이는 엔티티를 영속성 컨텍스트에 보관하지 않는 것을 의미하며, 스냅샷에 데이터를 보관하지 않는다. 이를 통해 메모리를 최적화할 수 있다.

아래는 예시 코드이다.

public interface ArticleRepository extends JpaRepository<Article, Long> {

    @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    List<Article> findAllByUserId(Long userId);
}

 

이렇게 두가지 방법으로 메모리를 최적화할 수 있다.

여기서 주의할 점이 있다. org.hibernate.readOnly 를 통한 방식은 영속성 컨텍스트에 엔티티를 보관하지 않도록 하는 방식이다. 그렇기에 repository 메서드로 엔티티를 가져온 후, 그 엔티티를 수정하고 플러시 해봤자 데이터는 수정되지 않을 것이다. 데이터가 수정되는 것은 uptate쿼리를 사용하지 않는 이상은 변경 감지를 통해서만 수정되기 때문이다.

잠시 변경 감지에 대해 살펴보자면, 플러시 시점에 스냅샷과 엔티티를 비교하고 차이가 있다면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보내고 쓰기 지연 저장소에 있는 SQL을 데이터 베이스에 보내게 된다. 이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지라 한다.

readOnly를 사용하면 영속성 컨텍스트에 스냅샷을 보관하지 않을 테니 수정이 일어나지 않는 것이다.

하지만 delete는 스냅샷과 상관없이 쓰기 지연 저장소에 쿼리가 보관되며 플러시를 할 때 바로 delete쿼리를 데이터베이스에 보내게 된다. 그렇기에 readOnly를 사용해도 delete는 적용이 된다는 것이다. 

 

 

속도 최적화

 

이제 속도를 최적화하는 방식을 살펴보겠다.

 

1. 읽기 전용 트랜잭션 사용

 

스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다. @Transactional을 사용할 때 readOnly 옵션이 있는데, 이를 아래와 같이 적용하면 끝이다.

@Transactional(readOnly = true)

 

트랜잭션이 종료될 때 데이터베이스를 커밋하기 전에 영속성 컨텍스트를 플러시 하여 영속성 컨텍스트와 데이터베이스를 동기화하게 된다. 그런데 readOnly 옵션을 true로 주면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다.

결국 아무리 영속성 컨텍스트에 있는 엔티티를 수정한다 해도 플러시를 실행하지 않으니 엔티티의 등록, 수정, 삭제는 당연히 동작하지 않는다.

이는 플러시 할 때 일어나는 스냅샷 비교와 같은 무거운 로직들을 수행하지 않는 것을 의미하므로, 당연히 속도 성능이 향상될 수밖에 없다. 

하지만 주의할 점이 있다. H2 데이터베이스에서는 위의 옵션이 적용이 안된다는 것이다. 다른 데이터베이스는 문제 없다. 만약 H2 데이터베이스로 로컬에서 배포할 시, 옵션이 적용이 되지 않는다고 해서 당황하지 않도록 하자.

자세한 사항은 https://github.com/scratchstudio/toby-spring/issues/7를 확인하길 바란다.

 

이를 테스트하는 방식은 다양하지만 단순히 readOnly옵션이 적용이 됐으면 true, 적용이 안됬으면 false를 리턴하는 메서드가 있다.

import org.springframework.transaction.support.TransactionSynchronizationManager;


@Transactional(readOnly = true)
public void testReadOnly() {
    log.info("Transaction readonly flag: {}",
        TransactionSynchronizationManager.isCurrentTransactionReadOnly());

    // do my task
}

 

위와 같은 방식으로 확인할 수 있다. 

 

2. 트랜잭션 밖에서 읽기

 

트랜잭션 밖에서 읽는다는 것은 트랜잭션 없이 엔티티를 조회한다는 것이다. 트랜잭션 내부에서는 트랜잭션을 커밋하거나 JPQL 쿼리를 실행하면 플러시가 작동된다. 하지만 트랜잭션 자체가 없으면 플러시가 일어나지 않는다. 

조회가 목적일 때만 사용을 하며, 이를 통해 속도를 최적화할 수 있다.

 

 

결론

 

이렇게 4가지 방식을 살펴보았다. 읽기 전용 방식은 크게 메모리 성능 향상과 속도 성능 향상 부분으로 나뉠 수 있다.

그리고 이 두 가지 방식을 동시에 사용하는 것이 가장 효과적이라고 한다.

앞으로 프로젝트를 진행할 때마다 상황에 맞게 잘 적용해야겠다.

 

 

참고 

 

자바 ORM 표준 JPA 프로그래밍 - 김영한

https://jaime-note.tistory.com/57

 

스프링 데이터 JPA - Hint & Lock

모든 소스 코드는 여기에서 확인 가능합니다. Hint SQL 에서 Hint 를 사용하듯이 JPA에서도 JPA 구현체 에 힌트를 전달할 수 있습니다. 조회만 사용하기(Read Only) 기본적으로 JPA를 이용해 데이터를 조

jaime-note.tistory.com

 

https://blog.leocat.kr/notes/2019/07/07/spring-check-transaction-readonly

 

[spring] transaction이 readonly인지 확인

현재 사용 중인 transaction이 정말 readonly로 설정되었는지 확인하고 싶을 때, TransactionSynchronizationManager#isCurrentTransactionReadOnly를 사용해서 확인이 가능하다.

blog.leocat.kr

 

https://github.com/scratchstudio/toby-spring/issues/7

 

@Transactional 의 read-only 작동하지 않음. · Issue #7 · scratchstudio/toby-spring

h2 인메모리 DB 를 사용할때는 read-only 속성의 트랜잭션 안에서 update 를 실행해도 예외가 발생하지 않고 성공한다. 반면, MySQL 에서는 TransientDataAccessResourceException 예외가 발생한다.

github.com