[졸업작품] spring boot로 작업한 뭉키를 다시 돌아보며 (3) - 트랜잭션의 readonly 옵션
2. Service에서 데이터를 가져오는 트랜잭션인 경우 readonly = true 옵션을 붙여야 영속성 컨텍스트가 변경 감지를 하지 않아 성능 향상
(1) Spring에서의 트랜잭션 처리
스프링 부트에서는 @Transactional 어노테이션을 사용하여 트랜잭션 처리를 할 수 있다.
@Transactional이 적용되어 있을 경우, 해당 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성된다.
이 프록시 객체는 @Transactioanl이 포함된 메소드가 호출 될 경우 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고 정상 여부에 따라 Commit 또는 Rollback 한다.
정상 여부는 RuntimeException 발생 유무 기준으로 결정되며 RuntimeExceptipn 외에 다른 예외(Ex. SQLException)에도 트랜잭션 롤백처리를 적용하기 위해서는 @Transactional의 rollbackFor 속성을 활용해야 한다.
@Transactional(readOnly = true, rollbackFor={Exception.class})
트랜잭션을 동작 도중 다른 트랜잭션을 호출(실행)하는 상황이 있다.
피호출 트랜잭션 입장에선 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고 새롭게 트랜잭션을 생성할 수도 있다.
호출한 쪽의 트랜잭션을 그대로 사용할 경우 중간에 오류가 발생한다면 모든 트랜잭션이 롤백된다.
반대로 새롭게 트랜잭션을 생성하면 오류가 발생한 트랜잭션만이 롤백된다.
이와 같은 트랜잭션 관련 설정은 @Transactional의 propagation 속성을 통해 지정할 수 있다.
- REQUIRED (default)
- 현재 진행중인 트랜잭션이 있다면 그것을 사용하고 없다면 생성한다.
- MANDATORY
- 현재 진행중인 트랜잭션이 없으면 Exception 발생. 없으면 생성한다.
- REQUIRES_NEW
- 항상 새로운 트랜잭션을 만들어 트랜잭션을 분리한다.
- SUPPORTS
- 현재 진행중인 트랜잭션이 있으면 그것을 사용하고 없으면 그냥 진행한다.
- NOT_SUPPORTED
- 현재 진행중인 트랜잭션이 있으면 그것을 사용하지 않는다 없으면 그냥 진행한다.
- NEVER
- 현재 진행중인 트랜잭션이 있으면 Exception. 없다면 그냥 진행한다.
[트랜잭션 주의사항]
트랜잭션은 꼭 필요한 최소한의 범위로 수행해야 한다.
왜냐하면 일반적으로 데이터베이스 커넥션은 갯수가 제한적이기 때문에 각 트랜잭션에서 커넥션을 소유하는 시간이 길어진다면 그 이후에 사용 가능한 커넥션의 갯수가 줄어들고 어느 순간 다른 트랜잭션이 수행될 때 커넥션이 부족하여 커넥션을 받기 위해 지연되는 상황이 발생할 수 있기 때문이다.
Reference
(2) 읽기 전용 readOnly = true 설정시 성능이 향상하는 이유
엔티티가 영속성 컨텍스트에 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있는 혜택이 많다.
단, 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용해야 하는 단점이 존재한다.
만약 조회만 하는 경우 읽기 전용으로 엔티티를 조회한다면 메모리 사용량을 최적화 할 수 있다.
@Transactional(readOnly=true) 를 통해 읽기 전용 트랜잭션을 사용한다면?
스프링 프레임워크가 하이버네이트 세션 플러시 모드를 MANUAL로 설정한다.
이렇게 되면 강제로 플러시를 호출하지 않는 이상 플러시가 일어나지 않는다.
따라서 트랜잭션을 커밋하더라도 영속성 컨텍스트가 플러시 되지 않아 엔티티의 등록, 수정, 삭제가 동작되지 않으며
또한 읽기 전용으로 영속성 컨텍스트는 변경 감지를 위한 스냅샷을 보관하지 않으므로 성능이 향상된다.
Reference
(3) 영속성 컨텍스트(Persistence Context)란?
엔티티를 영구 저장하는 환경
영속성 컨텍스트는 어플리케이션과 데이터베이스 사이에서 객체를 보관하는 논리적 개념이다.
EntityManager를 통해 영속성 컨텍스트에 접근한다.
EntityManger가 생성되면 논리적 개념인 영속성 컨텍스트가 1:1로 생성된다.
EntityMangerFactory는 요청이 올 때 마다 EntityManeger를 생성하는데 EntityManeger는 요청이 올 때마다 생성 비용이 거의 없기 때문이다. 반대로 EntityManegerFactory는 여러 스레드에서 동시에 접근해도 안전하다는 장점이 있지만 생성하는 비용이 상당히 크다.
EntityManeger는 스레드를 safe하지 않기 때문에 여러 스레드에서 접근하면 동시성 문제가 발생할 수 있으므로 요청(쓰레드)별 한 개씩 할당한다.
EntityManeger는 내부적으로 db connection를 사용하여 db를 사용한다. 단, db 연결이 필요한 시점까지는 connection을 얻지 않는다.
Entity의 생명 주기
- 비영속
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이다.
Member member = new Member("멤버");
- 영속
영속성 컨텍스트에 의해 관리되는 상태이다.
영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분하므로 영속 상태는 식별자 값이 반드시 존재해야 한다.
Member member = new Member("멤버");
EntityManager entityManager = entityManagerFactory.createEntityManger();
entityManger.persist(member); // 영속성 컨텍스트에서 관리한다.
단, EntityManger.persist(member);를 한다고 DB에 저장되는 것이 아니라 commit 혹은 flush() 이후 db에 저장된다.
그전까지는 영속성 컨텍스트에만 존재한다.
- 준영속
영속성 컨텍스트에 저장되었다가 분리된 상태이다.
개발자가 준영속을 직접 만들 일은 거의 없다.
Member member = new Member("멤버");
EntityManager entityManager = entityManagerFactory.createEntityManger();
entityManger.detach(member);
- 삭제
실제 DB에서 삭제된 상태이다.
Member member = new Member("멤버");
EntityManager entityManager = entityManagerFactory.createEntityManger();
entityManger.remove(member);
애플리케이션과 데이터베이스 사이 논리적 영속성 컨텍스트가 존재하는 이유?
1차 캐시
entityManager.persist(member); // 영속성 컨텍스트 1차 캐시에 member 저장
Member findMember = entityManager.find(Member.class, 1L); // 1차 캐시에서 조회
찾고자 하는 객체가 1차캐시에 존재하지 않는다면 DB에서 직접 조회 후 1차 캐시에 저장 후 반환한다.
요청이 시작되면 영속성 컨텍스트를 생성하고 요청이 끝난다면 영속성 컨텍스트를 지운다. 이때, 1차 캐시도 함께 지운다.
=> 애플리케이션 전체에서 공유하는 것이 아니다. 애플리케이션 전체에서 공유하는 캐시는 2차캐시이다.
동일성 보장(identify)
Member a = entityManager.find(Member.class, 100L);
Member b = entityManager.find(Member.class, 100L); // 똑같은 key 100L를 검색
a == b; // true
entityManager.find()를 반복 호출하여도 영속성 컨텍스트에 있는 같은 식별자를 가진 같은 엔티티를 반환한다.
따라서 영속성 컨텍스트는 엔티티의 동일성(==)을 보장한다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ)등급의 트랜잭션 격리 수준을 db가 아닌 애플리케이션 차원에서 제공한다.
트랜잭션을 지원하는 쓰기 지연
// member1, member2 객체 생성
// 엔티티 매니저 생성, 트랜잭션 획득
transaction.begin();
em.persist(member1);
em.persist(member2);
// INSERT SQL을 DB에 보내지 않음
transaction.commit();
// coomit을 수행하는 순간 INSERT SQL을 DB에 보낸다.
엔티티 매니저는 트랜잭션을 커미샇기 전까지 DB에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 트랜잭션이 커밋될 떄 모아둔 SQL을 DB에 보낸다. 이것이 쓰기지연이다.
트랜잭션을 커밋하면 영속성 컨텍스트는 flush를 하는데 여기서 flush는 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업이다.
바로 insert 쿼리를 db에 보내지 않고 1차 캐시에 먼저 저장하면서 동시에 쓰기 지연 SQL 저장소에 쌓아둔다.
트랜잭션이 커밋되는 순간 db에 insert 쿼리를 보낸다.
* flush() 호출 시 커밋과 별도로 저장될 수 있다.
변경 감지
// 엔티티 매니저 생성, 트랜잭션 획득
transaction.begin();
Member member = entityManager.find(Member.class, 10L);
member.setName("변경후");
transaction.commit(); // UPDATE 쿼리 전송
JPA는 변경 감지 기능을 통해 엔티티를 변경할 수 있다.
엔티티의 변경사항을 DB에 자동으로 반영하는 것이 변경감지이다.
변경감지는 영속성 컨텍스트가 관리하는 영속상태의 엔티티에만 적용되고 비영속, 준영속 상태의 엔티티는 적용되지 않는다.
JPA는 트랜잭션 되는 순간 내부적으로 flush()가 호출된다.
그때 엔티티와 영속성 컨텍스트의 1차 캐시 스냅샷(엔티티 최초상태)를 비교한다.
비교한 결과 변경이 있다면 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 저장한다.
커밋이 되면 flush() 호출 되어 DB에 UPDATE 쿼리가 나가게 된다.
변경감지로 인해 생성된 UPDATE 쿼리는 엔티티의 모든 필드를 업데이트한다.
모든 필드를 DB에 보내면 데이터 전송량이 증가하는 단점이 존재하지만 아래와 같은 장점을 얻을 수 있다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다. 애플리케이션 로딩시점에 수정쿼리를 미리 생성하고 재사용이 가능하다.
- DB에 동일한 쿼리를 보내면 DB는 이전에 한번 파싱된 쿼리를 재사용할 수 있다.
쓰기지연이란?
SQL쿼리를 내부저장소에 모아두었다가 트랜잭션이 커밋될 때 DB에 쿼리를 전달하는 것
지연 로딩(Lazy Loading)
연관 관계 매핑되어 있는 엔티티 조회 시 프록시를 반환함으로써 쿼리를 필요할 때 날리는 기능
플러시(flush)란?
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것
플러시가 발생하면
- 변경 감지
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 db에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시 하는 방법
- em.flush()
- 트랜잭션 커밋
- JPQL 쿼리 실행
플러시 주의 사항
- 영속성 컨텍스트를 비우지 않는다.
- 영속성 컨텍스트의 변경 내용을 db에 동기화
Reference