복습
과거에 10분 테코톡을 보고 트랜잭션에 관련된 내용을 정리한 적이 있었다. 그 내용을 우선 다시 복기해보자.
일단 트랜잭션은 한 요청에 대한 작업 단위라고 생각하면 쉽다.
이 트랜잭션이 수행될 때, 중요한 부분이 내부 동작이였는데 DB 커넥션 풀을 이용해서 커넥션 객체를 가지고 오게되고, 이 객체를 이용해서 DB에 요청을 보낸다고 했다. DB 입장에서는 이 커넥션을 하나의 세션으로 생각할텐데 한 기능의 작업 단위를 묶지않으면, 결국 제 각각의 다른 커넥션으로 생각할 것이고, 이는 세션을 처리할 때, 개별 작업으로 처리해버릴 것이다. 그래서 우리가 원하는 기대 값이 나오지 아니할 가능성이 높다.
여기서 가장 중요한 점은 결국 우리가 만들고자 하는 기능을 작업 단위 즉 트랜잭션으로 묶어야 한다는 점이 중요하다.
트랜잭션을 관리하는 방식에는 2가지 방식이 있다고 했는데 선언적 트랜잭션과 프로그래밍 트랜잭션이 있다.
선언적은 말 그대로 어노테이션 형태로 제공하는 것이고, 프로그래밍은 직접 트랜잭션을 관리하는 것이다.
직접 프로그래밍을 통해 트랜잭션을 관리하게 되면 중복 코드가 많이 발생할 것이고, 주된 관심사의 코드가 아닌 코드들이 서비스 레이어에 담길것이다. 그래서 해당 기능이 요구하는 관심사에 대해 집중하지 못할것이고, 코드 작성에 문제가 발생하거나 특정 기술에 종속적인 코드가 될 것이다.
스프링에선 직접 프로그래밍처럼 복잡하고 번거로운 작업을 프록시 객체를 빈에 등록해서 문제를 해결하고자 하며, 이 프록시 객체는 @Transactional이 포함된 메서드가 호출된다면 PlatformTransactionManger을 사용해 트랜잭션을 시작하고, 정상 여부에 따라서 커밋, 롤백의 흐름을 가지게 된다.
이 트랜잭션에서 성능상의 이슈가 많이 발생한다. 트랜잭션을 열게 되면 계속 DB를 물고있게 되는데, 이 부분에서 리소스를 많이 잡아먹는다.
더 나아가기
본론으로 들어가보자. 트랜잭션을 다시 꺼낸 이유는 다음과 같다.
우리는 트랜잭션을 사용할 때 보통 데이터와 관련된 작업을 진행할 때, 트랜잭션을 선언해서 사용할텐데 이것도 신중하게 사용해야한다. 그 이유는 조금전에 설명한 것처럼 트랜잭션을 열면 계속 DB를 물고있게 되는데, 이 커넥션 풀은 한정적이다. 그래서 이런 트랜잭션들이 계속 물고 있다면??? 다른 작업을 수행하지 못할 수 있다.
그래서 트랜잭션을 사용할 때 필요할 때 사용해야 한다라는 것을 꼭 기억해야한다.
여기서 우리는 보통 데이터를 Get하는 메서드에서는 트랜잭셔널 속성을 readOnly로 줄 것이다. 이 속성을 줌으로 인해서 트랜잭션을 읽기 속성으로 적용하는데, 이 때 성능상의 이점이 발생한다. 그래서 이 속성을 주고 데이터 삽입, 삭제, 수정을 하게 되면 런타임 오류를 만나볼 것이다.
그럼 이 @Transactional 속성을 ReadOnly로 주기만 하면 내부에 어떻게 동작하길래 성능상의 이점을 준다는 것인가?
일단 속성의 이름만 봐도 읽는거만 오직 이라는 것을 알 수 있다.
일단 엔티티 자체가 영속성 컨텍스트의 관리대상이 되면 1차캐시부터 변경 감지까지 많은 이점을 얻을 수 있다. 하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다
왜 엔티티 자체가 영속성 컨텍스트의 관리 대상이 되면 1차 캐시부터 변경감지까지 이점을 얻을 수 있지???
과거에 정리한 내용으로는 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이다 . 애플리케이션과 DB 사이에서 객체를 보관하는 논리적 개념인데, entityManager를 통해서 영속성 컨텍스트에 접근한다. (EntityManager가 생성되면 논리적 개념인 영속성 컨텍스트(PersistenceContext)가 1:1로 생성된다. )
엔티티가 영속성 컨텍스트의 관리 대상이 되는거라면 영속 상태라는 말인데 객체가 영속성 컨텍스트에 저장되어 있다. 그래서 영속 상태(Persist)로 만들면 객체는 1차 캐시에 저장되는데, 이때부터 성능상의 이점이 있다.
만약에 해당 객체를 조회하게 된다면 원래는 바로 DB에 접근해서 해당 객체를 찾아야 한다. 그러나 JPA에서는 영속성 컨택스트에 먼저 찾아보고 있으면 그 객체를 반환하고, 없으면 DB에 접근한다. 이 부분에 있어서 성능상의 이슈가 우선 발생한다.
또한 데이터를 바로 커밋하지 않고 쓰기지연 SQL 저장소에 쌓아두는데, 커밋이 되는 순간 데이터 관련 쿼리문을 날린다. ( Insert )
그리고 변경감지를 이용해 엔티티나 데이터의 변경을 감지한다. 동작원리는 다음과 같다.
먼저 JPA는 트랜잭션 되는 순간 내부적으로 flush()가 호출되는데, 이때 엔티티와 1차 캐시 내부의 스냅샷(최초 상태)를 비교한다.그래서 비교했을 때, 변경이 있으면 update 쿼리를 쓰기 지연 SQL에 저장하고 커밋이 되면 flush()가 호출되서 DB에 쿼리가 ㅂ라생하는 것이다.
마지막으로 LazyLoading ( 지연 로딩)이 지연 로딩은 연관관계를 맺고있는 엔티티의 조회 할 때, 프록시(가짜 객체)를 반환함으로써 쿼리를 진짜 필요로 할 때 날리는 기술이다. 이말 즉슨 원래 조회할때 뭐 조인을 하든 뭘 하든 같이 불러와야하는데, 해당 연관 데이터를 사용하기 전까지 쿼리문을 날리지 않음.
이렇게 트랜잭션의 속성을 readOnly로 주면 이런 이점이 있다.
학습한 내용을 정리해보면 일단 트랜잭션을 달지 않으면 효율적으로 운용이 가능하다. 달지않고 서비스 로직을 구성하게 된다면 필요할 때 db에게 요청해서 데이터를 얻어오는데 이 때 중요한 점은 필요할 때만 요청한 다는 것이다.그러나 트랜잭션을 달아놓게 되면 해당 서비스 로직이 끝날 떄 까지 DB를 물고있는 다는 점이다. 이 차이와 이슈를 꼭 알고 있어야 한다. (써야할 땐 써야겠지?)
이 트랜잭션을 readOnly 속성을 주지 않고도 읽기 전용으로 엔티티를 조회할 수 있는 방법들이 있다.
1. 스칼라 타입으로 조회
뭔말이냐? 엔티티가 아닌 스칼라 타입으로 필요한 필드를 조회하는 것인데, 엔티티 객체가 아니라서 영속성 컨텍스트가 결과를 관리하지 않는다
2. 읽기 전용 쿼리 힌트 사용
하이버네이트 전용 힌트인 org.hibernate.readOnly를 사용하면 읽기 전용으로 조회 가능. 하지만 읽기 전용이라서 영속성 컨텍스트는 스냅샷을 보관하지 않는다, 그래서 메모리 사용량을 최적화 가능하다는 점.
3. 읽기 전용 트랜잭션 사용 (readOnly 속성)
이 옵션을 주면 스프링 프레임워크가 하이버네이스 세션 플러시 모드를 MANUAL로 설정한다. 그래서 강제로 플러시를 호출하지 않는 이상 플러시가 일어나지 않는다. 그래서 트랜잭션을 커밋해도 영속성 컨택스트가 플러시 되지 않아서 수정, 삭제, 등록이 동작하지 않고, 영속성 컨텍스트는 변경 감지를 위한 스냅샷을 보관하지 않아서 성능이 향상된다.
마무리
참고 사이트 :
'스프링 > JPA' 카테고리의 다른 글
Cascade Type의 종류, 그리고 의도 (0) | 2023.03.22 |
---|---|
JPA 데이터베이스 초기화 전략 (5가지 ) (0) | 2023.03.22 |
count 그리고 성능과 서브쿼리(SubQuery) (1) | 2023.02.15 |
만약 대량의 데이터를 지워야 할 일이 있다면??? (JPA) (0) | 2023.01.14 |
cascade = CascadeType.REMOVE ? orphanRemoval = ture? (0) | 2023.01.06 |