정리: Spring Data Repository 객체의 save(Entity) 메서드를 가급적이면 반환된 Entity 객체를 사용하는 게 좋다.
JPQL 쿼리는 PK를 사용하지 않는 repository JPA 문을 사용 시에 flush()가 사용되므로, 1차 캐시와 db 간에 데이터차이가 발생하는 것에 주의하자. clear()를 사용하여 이 불일치를 해결하면 좋다.
영속성 컨텍스트는 JPA에서 제공되는 논리적 구조로, 영속 상태의 엔티티를 관리하는 데 사용된다.
JPA는 영속성 콘텍스트를 활용하여 주요 캐시, 식별성 보장, 쓰기 지연, 그리고 변경 감지(더티 체킹)와 같은 다양한 기능을 제공한다. (이 기능은 JPA의 무결성을 보호하는 데 목적이 있지만, 성능 저하의 원인 중 하나가 될 수도 있다.)
영속성 컨텍스트에 의해서 관리되는 Entity의 생명주기
- New/Transient(비영속 상태) : 영속성 컨텍스트와 아예 관련 없는 상태
- Managed(영속 상태) : 영속성 컨텍스트에 저장되어 관리를 받고 있는 상태
- Detached(준영속 상태) : Managed 상태였던 Entity 가 영속성 콘텍스트에서 분리된 상태
- Removed(삭제 상태) : 삭제된 상태
- 준영속 상태와 flush(), merge()에 대해서는 반드시 내부 동작방식을 이해하고 넘어가야 한다.
영속성 콘텍스트는 기본적으로 @Transactional의 라이프사이클과 함께 동작한다.
트랜잭션이 시작될 때 영속성 콘텍스트가 생겼다가, 트랜잭션이 종료될 때 영속성 콘텍스트가 사라진다고 생각하면 편할 것 같다. (OSIV라는 예외가 존재한다.)
EntityManager은 @PersistenceContext를 통해서 의존성 주입이 가능하고, 아래 요청 등을 통해 db에 작업을 수행할 수 있다.
- find(Enitty 클래스명, PK)
- 해당하는 Entity 객체를 Id(PK)를 통해 조회한다.
- 1차 캐시를 이용해 성능향상 및 동일성 보장을 제공한다고 한다.
- createQuery(JPQL)
- JPQL을 이용해 SQL 쿼리를 실행(1차 캐시 확인 없이 바로 db에 쿼리를 날린다)
- persist(Entity)
- 비영속 상태인 Entity 객체를 영속 상태인 Entity 객체를 영속 상태로 만든다.
- 쓰기 지연 SQL 저장소에 INSERT 쿼리를 생성한다.
- merge(Entity)
- 준영속 상태인 Entity 객체를 영속 상태로 만든다.
- 1차 캐시에 해당 Entity 객체에 대한 정보가 없으면 데이터베이스에 SELECT 쿼리를 날려 1차 캐시에 데이터를 생성한다.
- 1차 캐시에 생성된 Entity 객체와 merge(Entity) 요청시 전달한 Entity 객체를 합치고, 합친 새로운 객체를 1차 캐시에 저장한 후 반환하게 된다.
- 현재 사용 중인 save 함수를 보면 여기에서 persist와 merge의 차이에 대해서 알 수 있다.(해당 entity가 1차 캐시에 존재하면 merge, 아니면 persist를 사용한다.)
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity; // 여기서도 persist(Entity) 가 동일한 Entity 객체를 반환한다는 사실을 확인할 수 있다.
} else {
return entityManager.merge(entity);
}
}
- remove(Entity)
- 영속 상태의 Entity를 영속성 콘텍스트에서 제거한다.
- 쓰기 지연 SQL 저장소에서 DELETE 쿼리를 생성한다.
- flush()
- 쓰기 지연 SQL 저장소에 보관되어진 SQL 쿼리문을 실제 db에 날려 실행한다.
- 쓰기 지연 SQL 저장소를 통해 SQL 쿼리를 최적화할 수 있는 만큼 주의해야 할 부분도 분명히 존재한다.
- 3가지 방법으로 호출될 수 있다.
- 직접호출:
- EntityManager 객체의 flush() 메서드, Spring Data Repository 객체의 saveAndFlush() 메서드 직접 호출
- 자동 호출 2가지:
- Transaction 이 Commit 될 때 자동으로 호출된다
- JPQL 쿼리를 이용한 요청 시 자동으로 호출된다.
- 직접호출:
- 위 3가지 방법 중 3번째 "JPQL 쿼리를 이용한 요청 시 자동으로 호출”에에 대해서 확인하고 넘어가야 할 부분이 존재.
- 이 부분에서 1차 캐시와 db 간의 불일치가 발생할 수 있으니 주의해야 한다고 한다.(트러블슈팅 부분에서 추후 언급)
- PK로 조회하지 않는 경우 모두 JPQL 쿼리가 발생한다.
- JPQL 쿼리를 날리기 전에 flush()를 하는 이유
- JPQL 쿼리는 1차 캐시를 보지 않고 바로 데이터베이스에 SQL 쿼리를 날린다
- flush()를 해주지 않으면 1차 캐시와 실제 데이터베이스의 정보가 달라지게 된다.
- 이 상태에서 1차 캐시의 정보를 보지 않고 db의 정보를 조회하면 데이터 정합성이 깨지게 된다.
- 1차 캐시를 포함한 영속성 콘텍스트를 비우고 싶다면 clear()를 사용한다.
- clear()
- 영속성 컨텍스트를 비운다.
- 영속성 콘텍스트에 의해서 관리되고 있던 Entity 객체들이 모두 준영속(Detached) 상태가 된다.
- JPQL로 SQL 쿼리를 요청하게 될 경우 1차캐시와 데이터베이스에 저장된 정보의 불일치가 일어날 수 있다. 이때 1차 캐시를 비움으로써 이 데이터 불일치를 해결하기 위해 사용된다.
준영속
- 준영속(Detached) 상태는 3가지 방식으로 생성된다.
- detach(): 영속 상태인 Entity를 준영속 상태로 만든다.
- clear(): 모든 영속 상태인 Entity들을 준영속 상태로 만든다.
- close(): 영속성 콘텍스트가 닫힌다. 즉, 모든 영속 상태인 Entity 가 준영속 상태가 된다.
- 준영속 상태인 Entity는 영속성 콘텍스트의 관리를 받지 않는 상태기 때문에 Lazy Loading과 같은 작업을 하게 되면 예외(LazyInitalizedException)가 발상한다고 한다.
- 그렇기에 merge(Entity)를 사용하여 준영속 상태의 Entity를 다시 영속 상태로 변환하여야 한다.
- 파라미터로 전달한 Entity 객체의 @Id(Pk)가 1차 캐시에 존재하는 Key 인지 확인한다.
- 1차 캐시에 존재하지 않으면 데이터베이스에 SELECT 쿼리를 날려 1차캐시에 추가한다. (이미 1차캐시에 존재한다면 별도의 SELECT 쿼리를 날리지 않는다)
- 1차캐시에 저장된 Entity 객체와 파라미터로 전달된 Entity 객체를 합친다.
- 합친 결과로 새로운 Entity 객체를 생성하고 1차캐시에 저장한 후 반환한다.
- merge(Entity)에서 반환된 Entity 객체는 파라미터로 전달한 Entity 객체와 항상 다른 객체다.
- 그렇기에 merge(Entity)로 인한 버그가 생성될 수 있으며, 항상 save(Entity)에서 반환된 Entity 객체를 활용하는 것이 안전한 코드를 작성할 수 있는 좋은 습관일 것이라고 한다.
'Spring > JPA' 카테고리의 다른 글
JPA심화트랙(5) - 객체지향을 곁들인 JPA, QueryDSL 실전 활용법 (0) | 2024.04.09 |
---|---|
JPA심화트랙(4) - JPA 유용한 기능들 소개 (1) | 2024.04.03 |
JPA 심화트랙(3) - 연관관계 매핑 (2) | 2024.04.03 |
JPA 심화트랙(1) - ORM의 필요성 (0) | 2024.04.01 |