영속성 컨텍스트
영속성 컨텍스트는 데이터(Entity)를 영구 저장하는 환경이라는 뜻으로,
애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.
영속성 컨텍스트는 논리적인 개념으로 눈에 보이는 개념은 아니지만, EntityManager를 생성할 때 1대 1로 영속성 컨텍스트가 하나 만들어지고, EntityManager를 통해 영속성 컨텍스트에 접근할 수 있다. EntityManager 클래스의 persist(), find(), remove() 등의 메서드를 통해 영속성 컨텍스트에 접근하여 데이터를 핸들링할 수 있다.
이 영속성 컨텍스트의 이점으로는 대표적으로 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 5가지가 있다.
이 개념들은 아래서 다시 알아보고, "영속"이라는 단어에 대해 먼저 알아보자.
영속성(persistence)
영속성이 뭔지 알려면 엔티티의 생명주기에 대해 알아야 한다.
엔티티의 생명주기는 비영속, 영속, 준영속, 삭제 상태로 구분할 수 있다.
비영속(new / transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이다.
new 키워드를 통해 엔티티 객체를 새롭게 생성했을 때, 이 엔티티 객체는 비영속상태이다.
영속(managed)
영속성 컨텍스트에 관리되는 상태이다.
생성한 객체를 EntityManager.persist(entity)를 통해 영속성 컨텍스트에 등록한 상태이다.
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태이다.
삭제(removed)
영속성컨텍스트에서 삭제된 상태이다.
EntityManager em;
Member member = new Member(1L, "membername");
//-- member: 비영속 상태 --
em.persist(member);
//-- member: 영속 상태 --
em.detach(member);
//-- member: 준영속 상태 --
em.remove(member);
//-- member: 삭제 --
Spring Data JPA 기준 설명 추가
save(), saveAll()
이 메서드는 em.persist() 또는 em.merge()를 호출한다.
- 새로 생성된 엔티티의 경우(비영속 상태인 경우) : 저장(INSERT)
- 이미 존재하는 엔티티의 경우(영속 상태인 경우 또는) : 수정(UPDATE)
if(entity.getId() == null || "entity가 영속성 컨텍스트에 존재하지 않을 때(비영속)"){
em.persist(entity);
} else{
em.merge(entity);
}
findBy()
이 메서드는 em.find()를 호출한다.
- 식별자로 entity를 조회한다. JPA에서 식별자는 @Id로 지정된 필드이고, 이 필드로 동등성 여부를 판단한다.
- 영속성 컨텍스트에 존재하면 캐시에서 반환하고, 없으면 DB에서 조회한다.
- 조회된 entity는 영속 상태가 된다.
delete()
이 메서드는 em.remove()를 호출한다.
- 엔티티를 삭제 상태로 설정하고, 트랜잭션 커밋 시 실제 DB에 DELETE 쿼리를 날린다.
EntityManager 사용
flush() 등의 기능을 Spring Data JPA에서도 사용할 수 있다.
@Autowired를 통해 등록된 EntityManager Bean을 가져다 사용할 수 있다.
트랜잭션 도중 flush() 등 EntityManager 직접 조작이 필요한 경우 사용한다.
@Service
public class SomeService{
@Autowired
private EntityManager entityManager;
public void someMethod(){
entityManager.flush();
}
}
영속성 컨텍스트의 이점
위에서 영속성 컨텍스트의 이점으로는 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩이 있다고 하였다.
자세히 알아보자.
1차 캐시 - 엔티티 조회
persist() 메서드를 통해 member를 저장하면, 즉시 DB로 insert쿼리를 통해 저장되지 않는다.
대신, 영속성 컨텍스트의 1차 캐시라는 공간에 저장된다.
이후에 조회 시에는 DB에 직접 접근하여 해당 데이터를 찾는 것이 아닌 1차 캐시에서 먼저 해당 데이터를 찾아보게 된다.
Member member = new Member();
member.setId("member1"); // PK
member.setUsername("회원1");
em.persist(member); //1차 캐시에 저장
Member findMember = em.find(Member.class, "member1"); //1차 캐시에서 조회
그러나 만약 1차 캐시에서 없는 데이터를 조회하게 되면 어떻게 될까?
DB에 접근하여 해당 데이터를 찾아서 1차캐시에 저장한 뒤 반환한다.
member2를 찾고 싶은데, 1차 캐시에 존재하지 않는 경우이다.
- em.find("member2") 메서드로 1차 캐시에서 member2를 찾아보았지만, 1차 캐시에 존재하지 않는 것을 확인했다.
- DB에서 member2를 조회한다.
- DB에서 조회한 member2를 1차캐시에 우선 저장한다.
- 1차 캐시에 저장한 member2를 반환한다.
EntityManager em은 하나의 트랜잭션 내에서 생성하고, 해당 트랜잭션이 종료될 때 em도 종료시킨다.
따라서 트랜잭션이 종료되면, em과 함께 해당 영속성 컨텍스트도 종료된다. 그렇다면 영속성 컨텍스트 내에 존재하는 1차 캐시 또한 사라지게 된다.
따라서 이 1차캐시가 성능에 큰 도움이 되진 못한다.
동일성 보장
영속 엔티티의 동일성을 보장해주어서, 마치 데이터가 가리키는 주소가 같은 것처럼 '==' 비교가 가능해진다.
같은 데이터를 아래와 같이 두번 불러와도 1차 캐시에서 같은 데이터를 불러오게 되기 때문에 == 비교가 가능하다.
Member member = new Member();
member.setId(101L);
member.setName("helloJPA");
em.persist(member);
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
System.out.println("result : " + (findMember1 == findMember2));
result : true
쓰기 지연
트랜잭션이 커밋되기 전까지 DB로 쿼리를 날리지 않고, 쓰기 지연 SQL 저장소에 저장해뒀다가
트랜잭션이 커밋되는 시점에 저장해둔 SQL을 모두 DB로 보낸다.
persist(memberA) 코드가 실행될 때는, DB로 쿼리가 날라가지 않는다.
대신, 1차캐시에 memberA 엔티티를 저장하고, 쓰기 지연 SQL 저장소에 SQL을 저장한다.
이후에 트랜잭션이 커밋되면, 쓰기 지연 SQL 저장소에 모아뒀던 SQL을 DB로 보내게 된다.
이렇게 트랜잭션이 커밋되는 시점에 SQL을 모두 DB로 전달하는 것을 flush라고 부른다.
이렇게 SQL을 모아뒀다가 한번에 보내는 기능의 장점은
batch 처리를 하여 DB 접근 횟수를 줄이는 등의 성능 개선의 여지가 생긴다.
변경 감지(dirty checking)
엔티티가 변경되면, 따로 변경 내용을 저장하거나 수정하는 코드를 작성하지 않아도 해당 엔티티는 자동으로 변경된다.
Member member = em.find(Member.class, 101L);
member.setName("hello");
//em.update(member); //=> 이런 코드가 필요없다.
tx.commit();
영속성 컨텍스트에서 맨 처음 DB로부터 데이터를 조회해서 1차 캐시에 저장할 때, 스냅샷을 찍어놓는다.
이후에 트랜잭션이 커밋되는 시점에 현재의 엔티티와 스냅샷을 비교하여 UPDATE SQL을 자동으로 생성한다.
지연 로딩
JPA는 엔티티가 실제로 사용될 때 로딩하는 지연 로딩을 지원한다.
Member 엔티티의 필드에 Team team이 있다고 가정해보자.
@Entity
public class Member{
@Id
private Long id;
@ManyToOne
private Team team;
}
Member member = memberRepository.find(memberId); //(1)
Team team = member.getTeam(); //(2)
String teamName = team.getName(); //(3)
어찌 보면 Member 엔티티에 Team 엔티티 필드가 있으니 (1) 번 시점에 Member 엔티티와 Team 엔티티를 모두 DB로 부터 조회해야 할 것 같다.
그러나 지연로딩을 사용하면 엔티티 객체가 실제로 사용될 때 SELECT 쿼리를 날린다.
(1)번 시점에서는 Member에 대한 select 쿼리를 날리고,
Team에 대한 select 쿼리는 해당 객체가 실제로 사용되는 (3) 번 시점에서 날리게 된다.
즉시 로딩(EAGER)
즉시로딩은 (1) 번 시점에 모든 데이터를 조회해 오는 것이다.
상황에 따라 즉시로딩과 지연로딩 중에 선택해서 사용하면 된다.
@xxxToxxx(fetch = fetchType.EAGER) 옵션을 추가하면 해당 엔티티는 즉시 로딩하도록 설정된다.
예를 들어 Member 엔티티에서 Team 필드가 거의 사용되지 않는 경우에는 지연 로딩하도록 설정하고,
Member 엔티티에서 Team 필드가 자주 사용된다면 즉시 로딩하도록 설정하면 될 것이다.
플러시(flush)
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것을 말한다.
위에서 EntityManager는 영속성 컨텍스트와 1대1로 존재하고, 하나의 트랜잭션 내에서만 동작하고 제거된다고 했다.
따라서 트랜잭션이 커밋될 때, 영속성 컨텍스트가 종료되며 flush가 발생하게 된다.
flush 발생 시
- 변경 감지
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송 (SELECT, UPDATE, DELETE 쿼리)
flush 호출 방법
- em.flush() : 직접 호출
- 트랜잭션 커밋 시 : 자동 호출
- JPQL 쿼리 실행 : 자동 호출
flush 명령은 쌓여있던 변경사항, SQL 등을 DB에 반영하는 것이다.
따라서 flush가 호출된다고 1차캐시가 삭제되거나 영속성 컨텍스트가 종료된다거나 하는 일은 발생하지 않는다.
정리
1차 캐시
영속성 컨텍스트내에 위치하는 1차 캐시 내에서 조회가 가능하고, 1차 캐시에 없으면 DB에서 조회하여 1차 캐시에 올려놓는다. 또한, 하나의 트랜잭션 내에서 객체를 저장하면 1차 캐시로 저장이 되고, 트랜잭션이 종료되는 시점에 DB로 쿼리를 날려서 적용한다.
동일성 보장
데이터 조회 시 1차 캐시로 인해 '==' 비교가 가능해진다.
쓰기 지연
트랜잭션을 커밋하기 전까지 SQL 쿼리를 날리지 않고, 모아서 보낸다.
변경 감지(Dirty Checking)
데이터를 맨 처음 조회하여 1차 캐시에 저장할 때, 스냅샷을 찍어둔다.
이후에 트랜잭션이 커밋될 때, 현재의 엔티티와 스탭샷을 비교하여 변경된 것을 확인 후 자동으로 UPDATE SQL을 생성한다.
지연 로딩
엔티티가 실제 사용될 때, DB로 조회 쿼리를 날린다.
플러시
영속성 컨텍스트의 변경 내용을 DB에 동기화 하는 것이다.
(참고) 인프런 - 김영한 님 '자바 ORM 표준 JPA 프로그래밍 - 기본 편'
https://www.inflearn.com/course/ORM-JPA-Basic
'Database > JPA' 카테고리의 다른 글
[JPA] 연관관계 매핑 - @ManyToOne, @OneToMany (0) | 2023.10.06 |
---|---|
[JPA] @Entity - 필드와 컬럼 매핑의 여러가지 속성 (1) | 2023.10.04 |
[JPA] @Entity - 기본 키 매핑 (1) | 2023.10.04 |
[JPA] 엔티티 매핑 (0) | 2023.10.04 |
[JPA] ORM, JPA, Hibernate, Spring Data JPA의 관계 (0) | 2023.10.03 |