[JPA] 1차 캐시 작동 조건과 적용 범위
1차 캐시 작동 조건
JPA 1차 캐시 작동 조건은 다음과 같다.
1차 캐시는 PK로 조회하는 것이 아니면 작동하지 않는다.
영속성 컨텍스트에서 조회할 때 @Id 즉 PK로 등록하고 조회하기 때문이다.
1차 캐시(영속성 컨텍스트)는 다음과 같이 key-value 형태의 구조를 가진다.
- Key: DB의 PK
- Value: Entity
1차 캐시 적용 범위
JPA 1차 캐시의 적용 범위는 동일 트랜잭션, 동일 스레드 내 이다.
다시 말하면, 로직에 트랜잭션을 설정하지 않으면 1) JPA 1차 캐시는 동작하지 않는다.
1차 캐시가 동작하지 않기 때문에 2) 변경 감지도 적용되지 않는다.
나아가 3) 멀티 스레드를 이용한다면, 서로 다른 스레드 간에는 JPA 1차 캐시가 적용되지 않는다.
1. 1차 캐시
@Description("1차 캐시는 Transaction을 사용하지 않으면 적용되지 않는다.")
public String transaction_jpa_1cache() {
Stock byProductId = stockRepository.findByProductId(1L) // 1차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("before byProductId.getQuantity() = " + byProductId.getQuantity());
Stock byProductId2 = stockRepository.findByProductId(1L) // 2차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("byProductId2.getQuantity() = " + byProductId2.getQuantity());
return "finished";
}
실행 결과
아래와 같이
SELECT문이 2번 발생한다.
Hibernate:
select // 1차 조회
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
before byProductId.getQuantity() = 32
Hibernate:
select // 2차 조회
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
byProductId2.getQuantity() = 32
이번에는
메서드에 트랜잭션을 설정해보겠다.
@Description("1차 캐시는 Transaction을 사용하지 않으면 적용되지 않는다.")
@Transactional
public String transaction_jpa_1cache() {
Stock byProductId = stockRepository.findById(1L) // 1차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("before byProductId.getQuantity() = " + byProductId.getQuantity());
Stock byProductId2 = stockRepository.findById(1L) // 2차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("byProductId2.getQuantity() = " + byProductId2.getQuantity());
return "finished";
}
실행 결과
아래와 같이
SELECT 쿼리가 1번만 실행됐다.
Hibernate:
select
stock0_.id as id1_48_0_,
stock0_.product_id as product_2_48_0_,
stock0_.quantity as quantity3_48_0_
from
stock stock0_
where
stock0_.id=?
before byProductId.getQuantity() = 32
byProductId2.getQuantity() = 32
2. 변경 감지
아래의 코드에서는 메서드에 트랜잭션을 설정하지 않았다.
public String transaction_jpa_1cache() {
Stock byProductId = stockRepository.findByProductId(1L) // 1차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("before byProductId.getQuantity() = " + byProductId.getQuantity());
byProductId.add(1L); // DB update 작업
System.out.println("after byProductId.getQuantity() = " + byProductId.getQuantity());
Stock byProductId2 = stockRepository.findByProductId(1L) // 2차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("byProductId2.getQuantity() = " + byProductId2.getQuantity());
return "finished";
}
실행 결과,
아래와 같이
DB 변경 없이 (UPDATE 쿼리 없이)
SELECT 쿼리가 2번 발생한다.
Hibernate:
select // 1차 조회
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
before byProductId.getQuantity() = 31
after byProductId.getQuantity() = 32
Hibernate:
select // 2차 조회
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
byProductId2.getQuantity() = 31
이번에는 같은 메서드에 트랜잭션을 설정해보겠다.
@Transactional
public String transaction_jpa_1cache() {
Stock byProductId = stockRepository.findByProductId(1L) // 1차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("before byProductId.getQuantity() = " + byProductId.getQuantity());
byProductId.add(1L);
System.out.println("after byProductId.getQuantity() = " + byProductId.getQuantity());
Stock byProductId2 = stockRepository.findByProductId(1L) // 2차 조회
.orElseThrow(() -> new IllegalArgumentException("없음"));
System.out.println("byProductId2.getQuantity() = " + byProductId2.getQuantity());
return "finished";
}
실행 결과,
DB 변경이 발생한다 (update 쿼리가 발생한다).
UPDATE 발생 후에 조회시
select문이 발생한다.
Hibernate:
select
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
before byProductId.getQuantity() = 31
after byProductId.getQuantity() = 32
Hibernate:
update // DB 변경
stock
set
product_id=?,
quantity=?
where
id=?
Hibernate:
select
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
byProductId2.getQuantity() = 32
3. 멀티 스레드
public String nonblockin2g() {
Stock stock = new Stock(1L, 10L);
stockRepository.save(stock);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// ForkJoinPool 스레드 1
CompletableFuture<Void> plusFuture = CompletableFuture.runAsync(() -> {
try {
stockService.plus(stock);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// ForkJoinPool 스레드 2
CompletableFuture<Void> minusFuture = CompletableFuture.runAsync(() -> {
try {
stockService.minus(stock);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// ForkJoinPool 스레드 3
CompletableFuture<Void> multiplyFuture = CompletableFuture.runAsync(() -> {
try {
stockService.multiply(stock);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
futures.add(plusFuture);
futures.add(minusFuture);
futures.add(multiplyFuture);
CompletableFuture<Void> allFutures = CompletableFuture.allOf(plusFuture, minusFuture, multiplyFuture);
allFutures.join();
System.out.println("this is done");
Stock byProductId = stockRepository.findStockByProductId(1L);
System.out.println("stock.getQuantity() = " + byProductId.getQuantity());
return "finished";
}
각 메서드는 조회 없이 DB값 변경만 한다.
public void plus(Stock stock) throws InterruptedException {
Thread.sleep(2000);
stock.add(10L);
stockRepository.save(stock);
System.out.println("[plus] stock.getQuantity() = " + stock.getQuantity());
}
public void minus(Stock stock) throws InterruptedException {
Thread.sleep(2500);
stock.decrease(5L);
stockRepository.save(stock);
System.out.println("[minus] stock.getQuantity() = " + stock.getQuantity());
}
public void multiply(Stock stock) throws InterruptedException {
Thread.sleep(3000);
stock.multiply(2);
stockRepository.save(stock);
System.out.println("[multiply] stock.getQuantity() = " + stock.getQuantity());
}
실행 결과는 다음과 같다.
각 스레드에서 조회를 하지 않았음에도 조회 후 update 쿼리를 실행한다.
즉, 서로 다른 스레드 간에는 JPA 1차 캐시를 공유하지 않는다.
각각의 스레드는 각자의 독립적인 1차 캐시를 이용한다.
Hibernate:
insert
into
stock
(product_id, quantity)
values
(?, ?)
// ForkJoinPool 스레드 1
Hibernate:
select
stock0_.id as id1_48_0_,
stock0_.product_id as product_2_48_0_,
stock0_.quantity as quantity3_48_0_
from
stock stock0_
where
stock0_.id=?
Hibernate:
update
stock
set
product_id=?,
quantity=?
where
id=?
[plus] stock.getQuantity() = 20
// ForkJoinPool 스레드 2
Hibernate:
select
stock0_.id as id1_48_0_,
stock0_.product_id as product_2_48_0_,
stock0_.quantity as quantity3_48_0_
from
stock stock0_
where
stock0_.id=?
Hibernate:
update
stock
set
product_id=?,
quantity=?
where
id=?
[minus] stock.getQuantity() = 15
// ForkJoinPool 스레드 3
Hibernate:
select
stock0_.id as id1_48_0_,
stock0_.product_id as product_2_48_0_,
stock0_.quantity as quantity3_48_0_
from
stock stock0_
where
stock0_.id=?
Hibernate:
update
stock
set
product_id=?,
quantity=?
where
id=?
[multiply] stock.getQuantity() = 30
this is done
Hibernate:
select
stock0_.id as id1_48_,
stock0_.product_id as product_2_48_,
stock0_.quantity as quantity3_48_
from
stock stock0_
where
stock0_.product_id=?
정리
JPA의 특징인 1차 캐시와 변경 감지 기능은
동일 트랜잭션, 동일 스레드 내에서만 동작한다.