영속성이란,
엔티티가 영구적으로 저장되는 속성을 의미한다.
영속성을 가지지 않은 데이터는 서버가 종료되면 해당 데이터가 모두 사라진다.
영속성 컨텍스트
엔티티를 영구적으로 저장하는 환경을 의미한다.일종의 논리적인 개념이다.
EntityManager가 이 영속성 컨텍스트에 Entity를 보관하고 관리한다.
(엔티티 매니저는 JPA에서 가장 중요한 인터페이스 중 하나)
persist()메소드를 통해 컨텍스트에 등록한다.
EntityManager를 하나 생성할 때 영속성 컨텍스트 하나가 만들어지며, EntityManager를 통해 그 컨텍스트에 접근하고 관리할 수 있다.
하지만 JPA를 사용할 경우 구현체인 Hibernate가 엔티티 매니저를 생성하므로, 개발자가 직접 EntityManager를 구성하거나 생성할 필요가 없다.
JPA를 사용할 때는 기본적으로 하나의 영속성 컨텍스트를 사용하게 되는데, 이 때 하나의 트랜잭션 내에서 엔티티를 조회하거나 변경할 경우 이 영속성 컨텍스트 내에서 수행된다. 트랜잭션 종료시 영속성 컨텍스트도 함께 종료된다.
📌즉 쉽게 말하면 영속성 컨텍스트는 트랜잭션과 함께 동작하며, 트랜잭션 별로 각각의 영속성 컨텍스트가 생성되는 것이다.
만약 A메소드에서 B메소드를 호출하며 파라미터로 Entity를 넘기게 되면, A메소드에 있는 영속성 컨텍스트에서만 해당 엔티티가 관리된다.
JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 내에서 DB의 데이터를 가져오면, 이 데이터는 영속성 컨텍스트가 유지된 상태이다. 이 상태에서 해당 데이터 값을 변경하면 트랜잭션이 ❗끝나는❗ 시점에 DB에 반영된다.
만약 트랜잭션 내에서 예외가 발생하면 롤백된다. 롤백은 @Transactional어노테이션이 달린 메소드 내에서는 알아서 처리되고, 어노테이션이 붙어있지 않으면 자동으로 수행되지 않는다. 따라서 데이터 일관성을 위해 트랜잭션을 사용하는 작업에서는 어노테이션을 달아야 한다.
repositoy.findById()하여 엔티티를 조회한 뒤 entity.setXX()만 해주어도 JPA는 알아서 변경 사항을 감지하고, 변경된 필드만 update쿼리에 반영한다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void updateUserName(Long id, String name) {
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
user.setName(name);
}
}
이런 코드가 있다고 하자.
우선, findById로 user를 조회하는 과정에서 영속성 컨텍스트 내에 해당 엔티티가 존재한다면 DB에 접근하지 않고 바로 반환할 것이고(1차캐싱), 그렇지 않다면 DB에 접근하여 찾아온 뒤 1차 캐시에 저장하고 영속 상태가 된 해당 객체를 반환한다.
그리고 user.setName으로 엔티티에 변경상황이 생긴다고 치자.
EntityManager는 이 변경 사항을 flush()시점에 감지할 것이다.
그러면 엔티티를 Dirty상태로 표시해 두고 Dirty상태의 엔티티들을 데이터베이스에 업데이트 한다. 이 감지와 업데이트는 거의 동시에 연달아 일어난다.
영속성 컨텍스트에 새로 저장된 Entity를 DB에 반영하는 과정을 flush라고 한다.
flush()는 일반적으로 트랜잭션 커밋시 함꼐 실행되지만, 꼭 트랜잭션 커밋시에만 실행되는 건 아니다. 이는 즉 변경 상태 감지 및 업데이트가 꼭 트랜잭션 종료시 발생하는 건 아니라는 것을 의미한다.
영속성 컨텍스트
영속성 컨텍스트 내에 저장하는 캐시(id, 엔티티의 map)를 1차 캐시라고 한다. 영속 상태 Entity는 모두 이 캐시에 저장된다. 식별자(@Id)를 키, Entity를 밸류 형태로 저장한다. entityManager가 find()를 통해 엔티티를 찾으면 우선 1차 캐시에서 찾고, 없으면 DB에서 조회한 후 1차 캐시에 저장하고 영속 상태가 된 해당 객체를 반환한다.
같은 컨텍스트 내에서 같은 id를 가지면 그 객체는 동일하다.
Member1 == Member2
그러나 만약 영속성 컨텍스트를 벗어난다면,
같은 ID값으로 조회해도 결과는 같은 객체가 아니다!
https://katastrophe.tistory.com/111?category=1018549
영속성 컨텍스트의 객체 동일성에 대해 잘 정리된 링크이다.
바로 dB에 엔티티를 저장하지 않고, 영속성 컨텍스트 내의 SQL저장소에 쿼리를 저장해둔다. flush()하면 저장해두었던 쿼리를 통해 DB에 반영한다.
트랜잭션을 커밋하면 EntityManager는 영속성 컨텍스트를 flush()한다.
영속성 컨텍스트에는 이전 flush()때의 엔티티 상태를 복사하여 저장해둔 스냅샷이 존재한다.
flush()는 직접 호출할 수도 있고, 트랜잭션 커밋시/영속성 컨텍스트 종료시/JPQL쿼리 실행 직전에 JPA가 자동으로 호출한다.
JPA는 flush()시점에서 스냅샷과 엔티티를 비교하여 변경된 엔티티를 찾는다. 만약 있다면 각각 객체에 대한 수정 쿼리를 만들어 쓰기 지연 SQL저장소에 저장한 후, 한꺼번에 DB로 보내고 트랜잭션을 커밋한다.
이 ❗변경 감지는 영속성 컨텍스트가 관리하는 영속 상태 엔티티에만 적용❗된다. 바꿔 말하면, 준영속 상태의 객체는 아무리 수정해도 영속성 컨텍스트가 변경을 감지하지 못한다. 영속상태가 아닐 경우 먼저 1차캐시로 끌고와서 영속화 시켜야한다?
[영속화 예시]
❓궁금한 점: 엔티티 조회했는데 1차 캐시에 없으면 db에서 조회하고 1차 캐시에 저장하고 영속 상태가 된 객체를 반환한다고 배웠다. 그래서 위 링크 에서는 한번 조회를 하여 1차캐시로 끌고와서 영속화시켰다.
조회하는 방법 아니어도 영속화시킬 수 있는 방법이 있지 않나? 그냥 persist()하는게 아닌가? 졸리니까 다음에 알아보기~
이미 존재하는 엔티티를 수정하거나 삭제할 경우 persist를 사용하지 않아도 되지만, 영속성 컨텍스트에 존재하지 않는 엔티티를 저장하려면 persist를 해야 한다.
실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제로 사용할 때에 영속성 컨텍스트를 통해 값을 불러오는 방법이다.
연관 관계가 설정된 엔티티를 조회할 경우 해당 엔티티를 즉시 로딩하지 않고, 실제 사용될 때까지 로딩을 늦춘다.
복잡한 연관 관계를 다룬 엔티티를 다룰 때 유용하다.
만약 Team-Member가 일대다 관계로 연결되어 있다면, Team을 조회할 때 Member까지 다 조회하지 않고, Team만을 조회한 뒤 Member가 필요한 시점에 추가 쿼리를 날려서 조회한다.
기본값으로 로딩은 한번에 다 로딩하는 EAGER로딩으로 설정되어 있다.
@Entity
public class Member {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// getter, setter, constructor
}
하지만 이와 같이 LAZY로딩을 설정해둔다면, 사용할 떄에 조회하므로 성능상 이점이 있다.
영속성 컨텍스트 특징 정리