JPA

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

iksadnorth 2023. 9. 12. 18:17

해당 게시물은 책 '자바 ORM 표준 프로그래밍'을 읽고 작성했습니다.

👣 개요

해당 게시글은 JPA에서의 '트랜잭션과 락'과 '2차 캐시'에 대해 서술할 계획이다.

JPA는 대부분의 DB는 READ COMMITTED 수준의 격리 수준을 요구한다.
이것은 성능 상 큰 이점을 줄 수 있어도 정확도 면에서는 손해를 볼 수 있다.
JPA는 부분적으로 해당 격리 수준 이상의 격리 수준을 제공할 수 있다.
Lock이 그 역할을 수행하고 이것을 심도있게 살펴본다.

JPA는 영속성 컨텍스트를 이용해 요청 범위 내에서 같은 엔티티를 DB 2번 이상 호출하지 않도록
1차 캐시를 제공한다. 해당 캐시 덕분에 성능이 크게 늘어났지만 안타깝게도
'요청과 응답 사이'의 캐시를 도와주는 것이지 'Web App 차원'에서의 캐시를 제공하는 것은 아니기에
이전 요청을 위해 DB에서 조회했던 엔티티를 이후 요청에서도 또다시 DB에서 조회해야 한다.
이런 비효율성을 막기 위해 2차 캐시라는 서버 전역에 영향을 주는 캐시를 설정할 수 있다.
역시 이것에 대해 심도있게 살펴 본다.

 

👣 두 번의 갱신 분실 문제

우선 동시 수정 작업으로 인해 데이터 일관성을 해치는 경우에 대해 알아야 한다.

상황 가정 및 문제제기
트랜잭션 A가 데이터 M을 수정하고 있다.
이 때, 트랜잭션 B가 데이터 M을 데이터 M1로 수정하고 커밋한다.
그리고 트랜잭션 A가 데이터 M을 데이터 M2로 수정완료하고 커밋한다.
그렇다면 데이터 M은 M1이어야 할까? 아니면 M2여야 할까?

고려할 수 있는 Option 3가지
1. M2[마지막 커밋] 선택
2. M1[첫 커밋] 선택
3. M1과 M2를 적절히 병합한 M3를 선택 - git 코드 병합 방식.

보통은 2번 선택지를 선택한다.
왜냐 하면, 트랜잭션A는 데이터 M을 기반으로 데이터 M2으로 수정한 것이지
데이터 M1로 바뀔 줄 알았다면 M2으로 수정하지 않을 수 있기 때문이다.
트랜잭션 A가 성별이 여자인 경우, 할인율 적용해 가격을 낮추는 작업이고
트랜잭션 B가 성별을 남자로 바꾸는 작업이었다면
이것은 굉장히 데이터를 왜곡하는 작업이 될 것이다.

 

👣 낙관적 락 & 비관적 락

JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면 DB의 격리 수준이 READ COMMITTED여도
REPEATABLE READ를 가능하게 할 수 있다.  이것들은 Lock에 의해 실현될 수 있는데
JPA는 낙관적 락, 비관적 락 2가지를 제공한다.

낙관적 락은 데이터의 Version을 기록해서 중간에 간섭이 존재했는지 확인하는 방법이다.
예를 들어, 데이터를 수정하면 데이터의 Version값을 +1하는 정책을 가지고 있다면
트랜잭션 A가 데이터 M을 수정하기 전에 Version값이 10임을 확인하고
커밋 이후에 Version값이 10일 때만 정상적으로 수행되는 것을 의미한다.

비관적 락은 데이터를 수정하기 전에 데이터에 Lock을 걸어
중간에 데이터를 바꾸지 못하게 원천 차단하는 방법이다.
해당 방법은 정확성 면에서는 매우 우수하다.
수정 작업은 항상 다른 이에 의해 방해받지 않는다.

하지만 만약 먼저 수정에 들어간 트랜잭션 A가 처리 시간이 10분이 걸리고
트랜잭션 B는 0.01초, 트랜잭션 C는 0.01초, 트랜잭션 D는 .....
라고 한다면 해당 방법은 성능에 부정적인 영향을 줄 수 있다.
그리고 데드락의 위험이 도사리고 있다.

 

👣 @Version

JPA는 낙관적 락을 사용할 수 있도록
엔티티에 Version을 기입하는 필드를 매핑하도록 한다.

@Entity
public class Board {
    @Id
    private String id;
    ...
    
    @Version
    private Integer version;
}

위에서 언급햇듯 수정할 때마다 +1이 되는 칼럼이고 
수정을 끝마치고 트랜잭션 시작 시, 확인했던 값과 다르다면 예외를 일으키는 장치다.

@Version으로 관리하는 칼럼은 JPA가 직접 관리하므로 개발자가 임의로 수정하면 안된다.
하지만 벌크 연산에서는 버전을 무시하므로 버전 필드를 강제로 증가시켜야
오류가 발생하지 않는다.

update Member m set m.age = m.name + 1, m.version = m.version + 1;

 

👣 JPA 락 사용

JPA에서 Lock을 사용하는 방법은 다음과 같다.

em.find(Board.class, id, LockModeType.OPTIMISTIC);

// or

Board board = em.find(Board.class, id);
em.lock(board, LockModeType.OPTIMISTIC);

 

👣 JPA 비관적 락

비관적 락은 실제 DB 트랜잭션에 의존해서 Blocking Lock을 거는 방법이다.
SQL 쿼리에 Select Fro Update 구문을 사용해서 데이터에 락을 건다.

이렇게 함으로서 낙관적 락에서는 엔티티에만 적용되었던 락이
비관적 락에서는 스칼라 타입을 조회할 때도 적용된다. 

 

👣 2차 캐시

위에서 언급했듯 요청 세션에서만 유지되는 1차 캐시의 한계를 해결하기 위해
애플리케이션 전체에 적용되는 캐시인 2차 캐시를 적용함으로서 성능을 높일 수 있다.

@Cachable
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    ...
}

실제로 사용할 때는 위와 같이 엔티티 클래스에 @Cachable 어노테이션을 붙여주면 된다.

물론 캐시 방법에 대해 세세히 설정 가능하다 

 

👣 JPA 캐시 관련 API

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();
    public <T> T unwrap(Class<T> cls);
}

contains: 포함 여부를 알려줌
evict: 캐시를 제거함.
unwrap: Cache 조회.

 

👣 하이버네이트와 EhCache 적용

총 3가지의 캐시를 지원한다.

1. Entity 캐시
2. Collection 캐시
3. Query 캐시

@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @Cache(usage = CacheConcurrencyStragegy.READ_WRITE)
    @OneToMany(...)
    private List<Book> books = new ArrayList<>();
}

 

'JPA' 카테고리의 다른 글

15장 고급 주제와 성능 최적화  (0) 2023.09.09
14장 컬렉션과 부가 기능  (0) 2023.09.08
13장 웹 애플리케이션과 영속성 관리  (0) 2023.09.07
12장 Spring Data JPA  (0) 2023.09.06
영속성 컨텍스트 vs JPQL  (0) 2023.09.06