////
Search
Duplicate
🏓

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

1. 트랜잭션과 락

1. 트랜잭션과 격리 수준

트랜잭션은 ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야 한다.
트랜잭션은 원자성, 일관성, 지속성을 보장하나 격리성을 완벽히 보장하려면 동시청 처리 성능이 매우 나빠지기 때문에 격리 수준을 두어 수행한다.

2. 낙관적 락과 비관적 락 기초

JPA의 영속성 컨텍스트를 적절히 활용하면 데이터베이스가 READ COMMITTED여도 어플리케이션 수준에서 REPEATABLE READ가 가능하다.
JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정하며 만약 일부 로직에 더 높은 격리 수준이 필요하다면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
만약 사용자 A와 B가 같은 객체를 시간 차를 두고 갱신하면 나중에 수정한 사람의 것만 남게 될 것이다. 이를 두 번의 갱신 분실 문제라 한다.
이를 해결하기 위해 마지막 커밋만 인정하기, 최초 커밋만 인정하기, 충돌하는 갱신 내용 병합하기 등의 방법이 있다.

3. @Version

JPA가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 한다.
버전 정보를 사용하면 최초 커밋만 인정하기가 적용되며 적용 가능한 타입은 Long(long), Integer(int), Short(short), Timestamp가 있다.
엔티티에 버전 관리용 필드를 하나 추가하고 @Version 어노테이션을 붙여두면 된다. 버전은 엔티티의 값을 변경하면 하나씩 자동으로 증가한다.
엔티티 수정 시, 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다. 따라서 최초 커밋만 인정하기가 적용된다.
JPA가 버전 비교를 수행하는 방법은 단순한데, 엔티티를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러쉬하면서 WHERE 절에 엔티티의 버전 정보를 추가한다.

4. JPA 락 사용

JPA는 다양한 락을 제공하며 조회하면서 락을 걸수도 필요할 때 락을 걸 수도 있다.
락은 다음 위치에 걸 수 있다.
EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
Query.setLockMode()
@NamedQuery
JPA가 제공하는 락 옵션은 javax.persistence.LockModeType에 정의되어 있다.

5. JPA 낙관적 락

낙관적 락은 버전을 사용한다. 따라서 낙관적 락을 사용하려면 버전이 있어야 한다. 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있다.
⇒ JPA 사용 시 추천하는 전략은 READ COMMITTED 트랜잭션 격리 수준과 낙관적 버전 관리를 사용하는 것이다.
락 옵션 없이 @Version만 있어도 낙관적 락이 적용되며 락 옵션을 사용하면 락을 더 세밀하게 사용할 수 있다.
NONE: 락 옵션을 적용하지 않아도 @Version이 적용된 필드가 있으면 자동으로 낙관적 락이 적용될 때 옵션이다.
용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 한다.
동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가시킨다. 이대 데이터베이스의 버전 값이 현재 객체의 버전과 다르면 예외가 발생한다.
이점: 두 번의 갱실 분실 문제를 예방한다.
OPTIMISTIC: 엔티티를 수정해야 버전을 체크하지만 이 옵션을 사용하면 엔티티를 조회만 해도 버전을 체크한다. 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 변경되지 않음을 보장한다.
용도: 조회한 엔티티는 트랜잭션이 종료될 때까지 다른 트랜잭션에 의해 변경되지 않음을 보장한다.
동작: 트랜잭션 커밋 시, 버전 정보를 조회하여 현재 엔티티의 버전과 같은지 검증한다. 만약 같지 않으면 예외가 발생한다.
이점: OPTIMISTIC 옵션은 DIRTY READNON-REPEATABLE READ를 방지한다.
⇒ 그럼 격리 수준이 어플리케이션 단에서 REAPETABLE READ 수준으로 설정되는 건가?
OPTIMISTIC_FORCE_INCREMENT: 낙관적 락을 사용하면서 버전 정보를 강제로 증가한다.
용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있다. 즉 연관관계에 있는 객체 중 하나라도 변경되면 버전을 증가시킨다.
동작: 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킨다. 추가로 엔티티를 수정하면 버전 UPDATE가 또 발생한다. 총 2번 발생할 가능성이 있다.
이점: 강제로 버전을 증가해서 논맂거인 단위의 엔티티 묶음을 버전 관리할 수 있다.

6. JPA 비관적 락

JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법이다. 주로 PERSSIMISTIC_WRITE 모드를 사용한다.
비관적 락은 다음과 같은 특징을 가진다.
엔티티가 아닌 스칼라 타입을 조회하는 경우에도 사용할 수 있다.
데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
PERSSIMISTIC_WRITE: 비관적 락의 대표적인 옵션으로 데이터베이스에 쓰기 락을 걸 때 사용한다.
용도: 데이터베이스에 쓰기 락을 건다.
동작: select for update를 사용해서 락을 건다.
이점: NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.
PERSSIMISTIC_READ: 데이터를 읽기 전용으로 락을 걸 때 사용한다. 일반적으로 잘 사용하지 않는다.
MySQl: lock in share mode
PostgreSQL: for share
PERSSIMISTIC_FORCE_INCREMENT: 비관적 락 중 유일하게 버전 정보를 사용한다. 비관적 락이지만 버전 정보를 강제로 증가시킨다.

7. 비관적 락과 타임아웃

비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기한다. 무한정 기다릴 수 없으므로 타임아웃 시간을 줄일 수 있다.

2. 2차 캐시

JPA가 제공하는 애플리케이션의 범위의 캐시에 대해 알아보고 하이버네이트와 EHCACHE를 사용해서 실제 캐시를 적용해본다.

1. 1차 캐시와 2차 캐시

데이터베이스에 접근하는 과정에서 사용하는 네트워크 비용은 어플리케이션 서버에서 내부 메모리에 접근하는 시간 비용보다 수만에서 수십만 배 이상 비싸다.
따라서 조회한 데이터를 메모리에 캐시해두고 데이터베이스의 접근 횟수를 줄이면 어플리케이션 성능을 획기적으로 개선할 수 있다.
영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이것을 1차 캐시라고 하며 일반적인 웹 어플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
어플리케이션 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다.
하이버네이트를 포함한 대부분의 JPA 구현체들은 어플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라고 한다.
1차 캐시
1차 캐시는 영속성 컨텍스트 내부에 있는데, 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장된다. 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 동기화한다.
JPA를 스프링 프레임워크나 J2EE와 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할 때 영속성 컨텍스트도 종료한다.
⇒ 물론 OSIV를 사용하면 영속성 컨텍스트의 생명주기를 요청의 생명주기와 함께 한다.
1차 캐시는 끄고 켤 수 있는 오볏닝 아니며 영속성 컨텍스트 자체가 사실상 1차 캐시다.
2차 캐시
어플리케이션에서 공유하는 캐시를 JPA는 공유 캐시라 하는데, 일반적으로 2차 캐시라고도 부른다.
2차 캐시는 어플리케시연 범위의 캐시로 애플리케이션이 종료될 때까지 캐시가 유지된다. 분산 캐시나 클러스터링 환경의 캐시는 어플리케이션보다 더 오래 유지될 수도 있다.
2차 캐시를 적용하면 JPA는 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾는다.
⇒ 따라서 2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있다.
2차 캐시의 특징은 다음과 같다.
2차 캐시는 영속성 유닛 범위의 캐시다.
2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환한다.
2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않는다.

2. JPA 2차 캐시 기능

JPA 2.0에서 2차 캐시 표준을 정의했다.
캐시 모드 설정
2차 캐시를 사용하려면 엔티티에 javax.persistence.Cacheable 어노테이션을 사용하면 된다.
persistence.xmlshared-cache-mode를 설정하여 어플리케이션 전체에 캐시를 어떻게 적용할지 옵션을 설정할 수 있다.
캐시 모드는 javax.persistence.SharedCacheMode에 정의되어 있는데 보통 ENALBE_SELECTIVE를 사용한다.
캐시 조회, 저장 방식 결정
캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 된다.
캐시 조회 모드나 보관 모드에 따라 사용하는 프로퍼티와 옵션이 다르다.
각각 retrieveMode, storeMode 이름이다.
JPA 캐시 관리 API
JPA는 캐시를 관리하기 위한 javax.persistence.Cache 인터페이스를 제공하는데, 이는 EntityManagerFactory에서 획득할 수 있다.

3. 하이버네이트와 EHCACHE 적용

하이버네이트와 EHCACHE를 사용하여 2차 캐시를 적용할 수 있다.
엔티티 캐시: 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.
컬렉션 캐시: 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다.
쿼리 캐시: 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.
환경설정
하이버네이트에서 EHCACHE를 사용하려면 hibernate-ehcache 라이브러리를 추가하면 된다.
쿼리 캐시와 컬렉션 캐시의 주의점
엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다.
그리고 이 식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 찾는다.

3. 정리

트랜잭션의 격리 수준은 4단계가 있으며 격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따른 문제가 발생한다.
영속성 컨텍스트는 데이터베이스 트랜잭션이 READ COMMITTED 격리 수준이어도 어플리케이션 레벨에서 REPEATABLE READ를 제공한다.
JPA는 낙관적 락과 비관적 락을 지원한다. 낙관적 락은 어플리케이션이 지원하는 락이고 비관적 락인 데이터베이스 트랜잭션 락 메커니즘을 따른다.
2차 캐시를 사용하면 어플리케이션의 조회 성능을 극적으로 올릴 수 있다.