트랜잭션과 락, 2차 캐시

트랜잭션과 격리 수준

  • 트랜잭션은 ACID를 보장해야함
    • 원자성, 일관성, 격리성, 지속성
  • 트랜잭션은 원자성, 일관성, 지속성을 보장함
  • 문제는 격리성인데 격리성을 완벽히 보장하려면 트랜잭션을 거의 차례대로 실행해야함
  • 그러면 동시성 처리 성능이 매우 나빠짐
  • 이런 문제로 ANSI표준은 트랜잭션 격리 수준을 4단계로 나누어 정의
    • 트랜잭션 격리 수준은 다음과 같음
      • READ UNCOMMITED
      • READ COMMITED
      • REPEATEABLE READ
      • SERIALIZABLE
    • 격시 수준이 낮을수록 동시성 성능은 증가하지만 다양한 문제 발생가능
  • 격시 수준에 따른 문제점
    • DIRTY READ
    • NON-REPEATABLE READ
    • PHANTOM READ
  • 각 트랜잭션 수준에 따른 문제점
    • READ UNCOMMITED
      • 커밋하지 않은 데이터를 읽음, 이것을 DIRTY READ라 함
      • 트랜잭션1이 수정중인 데이터를 트랜잭션2가 조회
      • 트랜잭션1이 롤백시 문제 발생
    • READ COMMITED
      • 커밋한 데이터만 읽기 가능, DIRTY READ가 발생하지 않음
      • 하지만 NON-REPEATABLE READ발생
        • 트랜잭션 1이 회원 A를 조회 중인데 트랜잭션 2가 회원A를 수정하고 커밋
        • 트랜잭션 1이 동일한 회원 조회시 다른 데이터를 읽음, 이처럼 같은 데이터를 읽을 수 없는 상태를 NON-REPEATABLE READ라 함
    • REPEATEABLE READ
      • 한번 조회한 데이터를 반복해서 조회하면 같은 데이터를 조회
      • 하지만 PHANTOM READ 바생
        • 트랜잭션1이 10살 이하의 회원을 조회, 트랜잭션2가 5살 회원을 추가 하고 커밋
        • 트랜잭션1이 다시 조회시 5살회원이 추가된 결과를 조회, 이처럼 반복 조회시 결과 집합이 달라지는 것을 PHANTOM READ라 함
    • SERIALIZABLE
      • 가장 엄격한 트랜잭션 격리 수준
      • 동시성 처리 성능이 급격히 떨어질 수 있음
  • 애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITED 격리 수준을 기본으로 사용

낙관적 락과 비관적 락

  • JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면 데이터베이스 트랜잭션 READ COMMITED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATEABLE READ) 가 가능
  • 만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 됨
  • 낙관적 락
    • 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정하는 방법
    • 데이터베이스가 제공하는 락이 아닌 JPA가 제공하는 락을 사용
    • 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없음
  • 비낙관적 락
    • 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고보는 방법
    • 데이터베이스가 제공하는 락을 사용
    • 대표적으로 select for update 구문
  • 추가적으로 데이터베이스 트랜잭션 범위를 넘어서는 문제도 존재
    • 사용자 A와 사용자 B가 같은 데이터를 수정한다고 할때 늦게 수정을 시도한 데이터만 남게됨
    • 이것을 두 번의 갱신 분실 문제(second lost updates problem)이라 함
    • 이 경우 트랜잭션만으로는 문제를 해결할 수 없음, 다음과 같은 3가지 선택 방법이 존재
      • 마지막 커밋만 인정하기 (기본)
      • 최조 커밋만 인정하기 (JPA의 버전관리기능을 사용하면 쉽게 구현가능)
      • 충볼하는 갱신 내용 병합하기 (애플리케이션 개발자가 직접 병합 방법을 제공해야함)
  • @Version
    • JPA가 제공하는 낙관적 락을 사용하려면 @Version어노테이션을 사용해서 버전관리 기능을 추가해야함
    • 적용가능 타입
      • Long, Integer, Shor, Timestamp
    • 트랜잭션1이 조회한 엔티티를 수정하고 있는데 트랜잭션 2에서 같은 엔티티를 수정하고 커밋해서 버전이 증가하면 트랜잭션1이 커밋할때 버전이 다르므로 예외 발생
    • 최초 커밋만 인정하기가 적용됨
    • update시 where에 현재 버전정보를 조건에 추가하여 수행
      • 수정할 대상이 없으면 예외 발생
    • 버전은 엔티티의 값을 변경하면 증가
    • 연관관계가 존재할때는 연관관계의 주인 필드를 수정하는 경우 버전이 증가
    • 벌크 연산에서는 제공하지 않음
      • 버전을 갱신할려면 강제로 증가시켜야함
  • JPA 락 사용
    • JPA를 사용할때 추천하는 전략은 READ COMMITED 트랜잭션 격리 수준 + 낙관적 버전 관리
    • 락은 다음과 같은 위치에 적용가능
      • EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
    • LockModeType
      • OPTIMISIC
      • OPTIMISTIC_FORCE_INCREMENT
      • PESSIMITIC_READ
      • PESSIMITIC_WRITE
      • PESSIMITIC_FORCE_INCREMENT
      • NONE
      • READ
      • WRITE
  • JPA 낙관적 락
  • JPA 비관적 락

2차 캐시

  • 1차 캐시와 2차 캐시
    • 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이것을 1차 캐시라 함
    • 이것으로 얻을 수 있는 이점이 많지만, 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효
      • OSIV를 사용해도 클라이언트의 요청이 들어올 때부터 끝날 때까지만 1차 캐시가 유효
    • 따라서 애플리케이션 전체적으로보면 획기적으로 데이터베이스 접근 횟수를 줄이지는 못함
    • 하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라함

1차 캐시

  • 1차 캐시는 영속성 컨텍스트 내부에 존재
  • 엔티티 메니저로 조회, 변경시 모든 엔티티는 1차 캐시에 저장됨
  • 트랜잭션을 커밋하거나 플러시를 호출하면 1차캐시에 있는 엔티티의 변경사항이 데이터베이스에 동기화됨
  • JPA를 스프링 프레임워크와 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할때 영속성 컨텍스트도 종료
    • OSIV에서는 트랜잭션이 아닌 요청단위로 영속성 컨텍스트 관리
  • 1차 캐시는 끄고 켤 수 있는 옵션이 아님
  • 영속성 컨텍스트 자체가 사실상 1차 캐시
    • 같은 엔티티가 존재하면 엔티티를 그대로 반환, 동일성(a == b) 보장

2차 캐시

  • 애플리케이션을 종료할때 까지 유지되는 캐시
  • 2차 캐시를 적용하면 엔티티 메니저는 데이터를 조회할때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾음
  • 2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환
    • 만약 같은 객체를 반환하면 동시에 수정하는 문제가 발생할 수 있음
    • 객체에 락을 걸 수도 있지만 복사해서 반환하는거에 비해 비용이 너무 높음
  • 2차캐시는 다음과 같은 특징을 가짐
    • 영속성 유닛 범위의 캐시
    • 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환
    • 데이터베이스 기본 키를 기존으로 캐시하지만 영속성 컨텍스트가 다르면 동일성을 보장하지 않음

JPA 2차 캐시 기능

  • @Cacheable
  • 캐시 조회, 저장 방식 설정
  • JPA 캐시 관리 API
  • @Cache
    • 하이버네이트 전용인 @Cache 어노테이션을 사용하면 세밀한 캐시 설정 가능
      • usage
      • region
      • include
    • usage의 CacheConcurrencyStarategy
      • NONE
      • READ_ONLY
      • NONSTRICT_READ_WRITE
      • READ_WRITE
      • TRANSACTIONAL
    • 캐시 영역
    • 쿼리 캐시
    • 쿼리 캐시 영역
    • 쿼리 캐시와 컬랙션 캐시의 주의점