소개배경
나는 원래 Service Layer에서 비즈니스 로직을 작성할 때, @Transactional(readOnly=true)
설정을 거의 의무적으로 해왔었다.
성능이 향상된다는 것은 알고 있었지만, 해당 원리는 파악하지 못하고 관습적으로 사용하는 것을 지양하고자 포스팅하게 되었다.
Annotation
@Transactional(readOnly=true)
우선 해당 속성값을 사용할 경우 아래와 같은 이점을 얻을 수 있다고 정의되어 있다.
💡 예상치 못한 엔티티의 등록(insert),변경(update),삭제(drop)을 예방할 수 있고, 또한 성능을 최적화 할 수 있다.
하지만, 우리는 아직 풀지 못한 숙제가 있다.
"어떻게 성능이 최적화되는가?"
해당 동작원리에 대해 자세히 이해하기 위해선, 우선 JPA에 대한 기본 지식이 있어야 이해가 빠르다.
JPA의 1차 캐시 또는 변경감지 에 대한 사전지식이 부족하다면 해당 링크에 포함된 글을 읽어보길 권한다.
읽기 전용 쿼리(Select)의 성능 최적화 작업
엔티티가 영속성 컨텍스트(Persistent Context)
에 의해 관리되면, 1차캐시와 변경감지를 사용할 수 있다는 장점이 존재한다.
하지만, 변경감지는 내부 동작원리상 변경감지 이전의 원본을 스냅샷을 생성하는 Snapshot Pattern
을 사용하기 때문에 더 많은 메모리를 사용하는 단점이 존재한다.
하지만 변경감지가 아닌 1차캐시에서의 단순 조회작업이라면, 읽기 전용을 사용할 경우 메모리 사용량을 최적화할 수 있다.
readOnly Entity 조회 방법
읽기 전용 쿼리를 지원하는 방법은 생각보다 여러가지가 있었다.
필자의 경우에는 @Transactional
어노테이션의 속성 값만을 이용하였었는데, 다른 방법을 사용할 경우 명시적으로 사용이 가능하거나
querydsl 환경에서도 유용하게 사용할 수 있다고 생각하여 여러가지 방법을 소개하려한다.
1. 스칼라 타입으로 조회
스칼라 타입으로 엔티티 전체가 아닌, 엔티티의 필드 값만을 조회하는 방법이다.
해당 방법의 장접으로는 엔티티 객체가 아닌 필드 값을 조회하므로 영속성 컨텍스트
에 의해 결과 값을 관리 받지 않는다.
하지만, Native Query를 지양하는 나로서는 별로 좋은 방법이 아니라고 사려된다.
select u.id, u.username from u;
2. 읽기 전용 쿼리 힌트 사용
Hibernate
에서 제공하는 힌트 중 org.hibernate.readOnly
를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다.
읽기 전용이기에 영속성 컨텍스트는 스냅샷을 별도로 생성하지 않아, 기존의 문제인 메모리 최적화를 사용할 수 있다.
단, 스냅샷이 없기에 조회 외의 작업은 불가능하다.(= 엔티티를 수정하여도 DB에 반영되지 않음)
TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);
query.setHint("org.hibernate.readOnly", true);
3. 트랜잭션을 읽기전용으로 사용
해당 방법이 우리가 평소 사용해오던 방법으로 Spring Framework에서 제공하는 기능이다.
@Transactional(readOnly=true)
해당 어노테이션을 Service Layer 상단에 사용할 경우 해당 Service Layer에 종속된 모든 로직은 읽기 전용으로 실행이된다.
해당 속성을 사용할 경우 Spring Framework에서는 Hibernate의 Session Flush 모드를 강제로 Manual로 설정한다.
Manual로 설정할 경우 강제로 flush()
를 호출하지 않는 이상, 플러쉬가 일어나지 않는다.
따라서 현재 작업중인 트랜잭션에 Commit
이 일어나더라도 영속성 컨텍스트에 플러쉬가 일어나지 않기 때문에, 생성,수정,삭제 작업 모두 DB에 반영되지 않는다.
또한, 읽기 전용이기에 영속성 컨텍스트에서 변경감지를 위한 스냅샷을 보관하지 않아 메모리 최적화 또한 지원된다.
💡 Hibernate에서는 기본적으로 4가지의 플러시 모드를 제공한다.
1.AUTO : 기본 값으로, Commit이나 쿼리 실행 시점에 flush된다.
2.COMMIT : 트랜잭션 커밋 시점에 flush.
3.ALWAYS : 모든 SQL 쿼리 실행 시점에 flush.
4.MANUAL : 개발자가 직접 flush를 호출해야 한다.
참고적으로, 하이버네이트 세션 객체를 구하려면, EM(Entity Manager)의 unwrap() 메서드를 호출하여 구할 수 있다.
Session session = entityManager.unwrap(Session.class);
4. 트랜잭션 밖에서 읽기
해당 방법은 트랜잭션을 사용하지 않는 것이다.
"엥 트랜잭션을 사용안하면 DB작업이 안되지 않나요?"
그렇다. 트랜잭션을 이용하지 않을 경우 DB에 생성/수정/삭제 작업은 일어나지 않는다.
하지만, 조회작업은 가능하므로 트랜잭션의 사용자체를 지양하여, 조회작업 로직만 별도로 분리하는 것이다.
트랜잭션을 사용하지 않는다면, 기본적으로 flush가 호출되지 않기 때문에 조회 성능이 향상된다.
결론
읽기 전용 데이터를 조회할 때, 메모리를 최적화의 목적으로 읽기 전용을 사용한다.
기본적으로 제공되는 여러방법이 존재하지만, Spring Framework에서 제공하는 읽기 전용 트랜잭션을 사용하는 것이 제일 효율적이라고 생각한다.
위에서 트랜잭션 밖에 읽는 방법은 개인적으로 추천하지 않는 방식이다. 그 이유는 평소 도메인 별 패키지의 분리를 선호하기 때문이다.
그 이유는 도메인 패키지안에 해당 도메인과 관련된 코드를 모아놓는 것을 유지보수적 관점에서 이점이 크다고 생각하기 때문인데, 조회 작업만을 별도로 분리한다면 불필요한 클래스의 증가가 발생한다.
"그러면, 도메인 별로 분리하되 조회 로직만 별도로 분리하는 것은요?"
그것 또한 결국 분리를 하는 순간 클래스의 증가는 불가피하다, 물론 프로젝트의 규모가 증가함에 따라 해당 방법이 더 좋은 효율성을 불러올 수 있다.
하지만, 현재 내가 운영중인 프로젝트 수준에서는 불필요하다 여겨진다.
'Spring-boot' 카테고리의 다른 글
Blocking vs Non-Block , Sync vs Async 그리고 푸른 수염의 SSE (0) | 2023.07.31 |
---|---|
[JPA] Data JPA에서 Projection 활용기 (0) | 2023.06.28 |
OSIV(Open-Session-In-View) 사용 (0) | 2023.03.16 |
profile을 적용하여 yaml 파일을 목적에 맞게 분리 (0) | 2023.03.06 |
CustomAnnotation을 이용하여 가독성과 불필요한 Import 줄이기 (0) | 2023.03.05 |