Chapter16. 트랜잭션과 락, 2차 캐시

김신영·2023년 2월 12일
0

JPA

목록 보기
13/14
post-thumbnail

Transaction이 보장하는 4가지 특성

Atomicity (원자성)

트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 혹은 실패이어야 한다.

Consistency (일관성)

모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.

Isolation (격리성)

동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야 한다.

Durability (지속성)

트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.

  • Transaction은 원자성, 일관성, 지속성을 보장한다.
  • 문제는 격리성

트랜잭션 격리 수준 (Isolation Level)

ANSI 표준은 트랜잭션의 격리 수준(Isolation Level)을 4단계로 나누어 정의했다.

  • READ UNCOMMITED (커밋되지 않은 읽기)
  • READ COMMITED (커밋된 읽기)
  • REPEATABLE READ (반복 가능한 읽기)
  • SERIALIZABLE (직력화 가능)

격리 수준에 따른 문제점

  • Dirty Read
  • Non Repeatable Read
  • Phantom Read

트랜잭션 격리 수준과 문제점

Read Uncommited

  • Dirty Read를 허용하는 격리 수준

트랜잭션 1이 데이터를 수정했고, 커밋만 하지 않은 상태일 때,
트랜잭션 2가 트랜잭션 1이 수정 중인 데이터를 조회 할 수 있다.

이것을 Dirty Read 라고 한다.

Read Commited

  • 커밋한 데이터만 읽을 수 있는 격리 수준
  • Dirty Read 는 허용하지 않는다.
  • 👌 Non-Repeatable Read 는 허용한다.

트랜잭션 1일 회원정보 A를 조회 중이고,
트랜잭션 2가 회원정보 A를 수정하고 커밋했을 때,
트랜잭션 1이 다시 회원정보 A를 조회했을 때 트랜잭션 2가 수정한 데이터가 조회된다.

이것을 Non-Repeatable Read 라고 한다.

Repeatable Read

  • 트랜잭션 내에서 한 번 조회한 데이터는 반복해서 조회해도 같은 데이터가 조회된다.
  • Non-Repeatable Read 는 허용하지 않는다.
  • 👌 Phantom Read 는 허용한다.

Serializable

  • 가장 엄격한 트랜잭션 격리 수준이다.
  • Phantom Read 가 발생하지 않는다.
  • But, 동시성 처리 성능이 극격히 떨어질수 있다.

트랜잭션 격리 수준에 따른 동작 방식은 데이터베이스마다 조금씩 다르다.

최근에는 더 많은 동시성 처리를 위해 Lock보다는 MVCC(Multiversion Concurrency Control)을 사용하기도 한다.

낙관적 락과 비관적 락

Optimistic Lock

  • 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정
  • ❌  데이터베이스가 제공하는 락 기능을 사용하지 않는다.
  • ✅  JPA가 제공하는 버전 관리 기능을 사용 (@Version )
  • 따라서, 낙관적 락은 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.

Pessimistic Lock

  • 트랜잭션의 충돌이 발생한다고 가정
  • 데이터베이스가 제공하는 락 기능을 사용한다.
    • select for update

두 번의 갱신 분실 문제 (second lost updates problem)

사용자 A와 B가 동시에 제목이 같은 공지사항을 수정할 때, 생기는 문제 해결하는 방법

  1. 마지막 커밋만 인정하기
  2. 최초 커밋만 인정하기
  3. 충돌하는 갱신 내용 병합하기

@Version

JPA가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 한다.

@Version 적용 가능 타입은 다음과 같다.

  • Long

  • Integer

  • Short

  • Timestamp

  • 버전은 엔티티의 값을 변경하면 증가한다.

  • 임베디드 타입과 값 타입 컬렉션을 수정해도, 엔티티의 버전이 증가한다.

  • 단, 연관관계 필드는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.

  • 벌크 연산은 버전을 무시한다. 벌크 연산에서 버전을 증가하려면, 버전 필드를 강제로 증가시켜야 한다.

javax.persistence.LockModeType 속성

낙관적 락 (Optimistic Lock)

낙관적 락에서 발생하는 예외는 다음과 같다.

  • javax.persistence.OptimisticLockException
  • org.hibernate.StaleObjectStateException
  • org.springframework.orm.ObjectOptimisticLockingFailureException

LockModeType.NONE

  • 조회 시점부터 수정 시점까지 조회한 엔티티가 다른 트랜잭션에 의해 변경되지 않음을 보장한다.
  • 즉, 엔티티를 수정을 하고 커밋할 때, 버전 정보를 확인.
  • 👍 두 번의 갱신 분실 문제를 예방

LockModeType.OPTIMISTIC

  • 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장한다.
  • 단순히 조회만 해도 커밋할때, 버전 정보를 확인.
  • 👍  Dirty Read , Non-Repeatable Read 를 방지

LockModeType.OPTIMISTIC_FORCE_INCREMENT

  • 엔티티를 수정하지 않아도, 트랜잭션을 커밋할 때 버전 정보를 강제로 증가시킨다.
  • 추가로 엔티티를 수정하면, 총 2번의 버전 증가가 발생할 수 있다.

비관적 락 (Pessimistic Lock)

  • 데이터베이스 트랜잭션 Lock에 의존한다. ( SELECT FOR UPDATE )
  • 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
  • 테이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
  • 락을 획득할 때 까지 트랜잭션이 대기하므로, Timeout을 설정할 수 있다.

비관적 락에서 발생하는 예외는 다음과 같다.

  • javax.persistence.PessimisticLockException
  • org.springframework.dao.PessimisticLockingFailureException

LockModeType.PESSIMISTIC_WRITE

  • 데이터베이스에 쓰기 락을 건다. (SELECT FOR UPDATE )

LockModeType.PESSIMISTIC_READ

LockModeType.PESSIMISTIC_FORCE_INCREMENT

2차 캐시

1차 캐시 (Persistence Context) 동작 방식

2차 캐시 동작 방식

  • 영속성 유닛 범위의 캐시
  • ❌  조회한 객체를 그대로 반환하는 것이 아니다.
  • ✅  조회한 객체의 복사본을 만들어서 반환한다.
  • 2차 캐시는 데이터베이스 Primary Key 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않는다.

캐시 모드 설정

  • persistence.xml
<persistence-unit name="test">
	<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</persistence-unit>
  • bean.xml
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
	<property name="sharedCacheMode" value="ENABLE_SELECTIVE"/>
	...
</bean>
  • application.yaml
spring:
  jpa:
    properties:
      javax:
        persistence:
          sharedCache: 
            mode: ENABLE_SELECTIVE 

캐시 조회, 저장 방식 설정

캐시 조회 방식

public enum CacheRetrieveMode {

    /**
     * 캐시에서 조회한다. 기본값
     */
    USE,

    /**
     * 캐시를 무시하고 데이터베이스에서 직접 접근해서 조회한다.
     */
    BYPASS  
}

캐시 저장 방식

public enum CacheStoreMode {

    /**
     * 조회한 데이터를 캐시에 저장한다.
     * 조회한 데이터가 이미 캐시에 있으면 캐시 데이터를 최신 상태로 갱신하지 않는다.
     * 트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장한다.
     */
    USE,

    /**
     * 캐시에 저장하지 않는다.
     */
    BYPASS,

    /**
     * USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시한다.
     */
    REFRESH
}

JPA 캐시 관리 API

EntityManagerFactory emf = em.getEntityManagerFactory();
Cache cache = emf.getCache();
public interface Cache {

    /**
     * 해당 엔티티가 캐시에 있는지 여부 확인
     */
    public boolean contains(Class cls, Object primaryKey);

    /**
     * 해당 엔티티중 특정 식별자를 가진 엔티티를 캐시에서 제거
     */
    public void evict(Class cls, Object primaryKey);

    /**
     * 해당 엔티티 전체를 캐시에서 제거
     */
    public void evict(Class cls);

    /**
     * 모든 캐시 데이터 제거
     */
    public void evictAll();

    /**
     * JPA Cache 구현체 조회
     */
    public <T> T unwrap(Class<T> cls);
}

Hibernate와 Ehcache 적용

  • 하이버네이트가 지원하는 캐시는 크게 3가지가 있다.

1. 엔티티 캐시

  • 엔티티 단위로 캐시
  • 식별자로 엔티티 조회할 때 사용
  • 컬렉션이 아닌 연관 엔티티 로딩할 때 사용

2. 컬렉션 캐시

  • 엔티티와 연관된 컬렉션을 캐시
  • 컬렉션이 엔티티를 담고있으면, 식별자 값만 캐시

3. 쿼리 캐시

  • 쿼리와 파라미터 정보를 키로 사용해서 캐시
  • 결과가 엔티티라면, 식별자 값만 캐시

설정 파일

  • pom.xml
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-ehcache -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.6.15.Final</version>
</dependency>
  • ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         maxBytesLocalHeap="300M">

    <!--<diskStore path="java.io.tmpdir" />-->

    <!--<sizeOfPolicy maxDepth="100000"/>-->

    <defaultCache
						maxElementsInMemory="10000"
						eternal="false"
						timeToIdleSeconds="1200"
            timeToLiveSeconds="1200"
						diskExpiryThreadIntervalSeconds="1200"
            memoryStoreEvictionPolicy="LRU">
		</defaultCache>

    <cache name="guelLocalCache"
           timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LRU">
		</cache>

</ehcache>
  • application.yaml
spring:
  jpa:
    properties:
      javax
        persistence:
          sharedCache: 
            #required - enable selective caching mode - only entities with @Cacheable annotation will use L2 cache.
            mode: ENABLE_SELECTIVE 
      hibernate:
        #optional - generate statistics to check if L2/query cache is actually being used.
        generate_statistics: true
        cache:
          #required - turn on L2 cache.
          use_second_level_cache: true
          #optional - turn on query cache.
          use_query_cache: true 
          region:
            #required - classpath to cache region factory.
            factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory

@org.hibernate.annotations.Cache

  • usage
    • CacheConccurrencyStrategy 를 사용해서 캐시 동시성 전략 설정
  • region
    • 캐시 지역 설정
  • include
    • 연관 객체를 캐시에 포함할지 선택

💡 쿼리 캐시와 컬렉션 캐시의 주의점

쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티는 꼭 엔티티 캐시를 적용해야 한다.**

profile
Hello velog!

0개의 댓글