해당 내용은 인프런의 '자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)'을 참고하여 작성하였습니다.
JPA의 핵심
1. 객체와 관계형 데이터베이스 매핑하기(Object Relation Mapping)
2. 영속성 컨텍스트
JPA에서 중요한 2가지는 저번 포스팅(JPA 시작하기)에서 간단하게 살펴본 바와 같이
객체와 RDB를 매핑하는 과정이다.
또한 중요한 것은 영속성 컨텍스트와 이것의 동작 과정이다.
영속성 컨텍스트
- JPA를 이해하는데 가장 중요한 용어
- "엔티티를 영구 저장하는 환경"의 의미
- EntityManager.persist(entity);
→ 이 코드를 통해 객체를 쉽게 다룰 수 있게 된다. - 논리적 개념 → (동작 과정이) 눈에 보이지 않음
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있다.
기본을 익히기 위한 간단한 실습에서는 하나의 Entity Manager와 영속성 컨텍스트의 1:1 관계를 알아볼 것이다.
엔티티의 생명주기
- 비영속성(new/transient)
- 영속성 컨텍스트와 전혀 관계가 없는 상태
- 객체가 새롭게 만들어졌을 때의 상태를 말할 수 있다.
- 영속(managed)
- 영속성 컨텍스트에 관리되는 상태
- 관리되고 있는 상태라는 말은 JPA에서 객체를 관리하고 있다는 말이다.
- 준영속(detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(remove)
- 영속성 컨텍스트에서 삭제된 상태
비영속 / 영속
객체가 선언된 후 영속 컨텍스트에 포함되지 않아 JPA에 의해 관리되지 않는 초기 상태를 예로 들 수 있다.
객체를 영속 상태로 만들기 위해서는 EntityManager.persist()를 사용하면 된다.
객체가 영속 상태가 되면 JPA에서 해당 객체를 관리할 수 있다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 비영속
Member member1 = new Member();
member1.setId(101L);
member1.setName("HelloJPA");
// 영속
em.persist(member1);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
참고로 member 객체가 영속성 컨텍스트에 저장되었다가 하더라도 DB에 member1 객체가 바로 insert 되는 것이 아니다.
DB에 insert 되는 시점은 트랜잭션이 commit 되는 시점으로
em.persist(member1); 앞뒤로 줄을 그어 줄력하면 쿼리가 더 나중에 실행되는 것을 확인할 수 있다.
❓ 그렇다면 왜? 트랜잭션이 commit 되는 시점에 DB에 SQL 쿼리가 실행되는걸까?
✅ 이유는 해당 매커니즘이 영속성 컨텍스트가 갖고 있는 이점 중 하나이기 때문이다.
✨ 영속성 컨텍스트의 이점
1) 1차 캐시
2) 동일성(identity) 보장
3) 트랜잭션을 지원하는 쓰기 지원(transactional write-behind)
4) 변경 감지(Dirty Checking)
5) 지연 로딩(Lazy Loading)
영속성에 대해 알아보기 위해 아래의 예제를 이용해보자.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
영속 컨텍스트 내부에는 1차 캐시가 존재한다.
member1 객체가 생성되고 persistence 되면서 객체의 Id와 객체 자체가 1차 캐시에 올라간다.
이때 .find 메소드를 사용하여 member1의 데이터를 select 하게 되면
member1의 데이터를 데이터베이스에서 select 하는 것이 아닌
1차 캐시에서 불러오게 된다.
만일, select를 할 때 1차 캐시에 데이터가 저장되어 있지 않다면 아래와 같은 과정을 통해 데이터를 select 해온다.
Member findMember2 = em.find(Member.class, "member2");
Id값이 member2인 데이터가 DB에 존재한다고 가정한다.
Id 값이 member2인 데이터는 한 번도 select 된 적이 없기 때문에 1차 캐시에 저장되어 있지 않은 상태이고 데이터를 DB에서 조회해온다.
이후 조회한 데이터를 1차 캐시에 저장하고 데이터를 반환한다.
member2 데이터가 1차 캐시에 저장되었기 때문에 다시 select를 하게 될 경우, DB에서 조회하는 것이 아닌 1차 캐시에서 select 한다. (이때 select 쿼리가 출력되지 않는다.)
영속 Entity의 동일성 보장
JPA는 자바 collection과 같이 객체를 비교할 수 있게 해준다.
Member findMember1 = em.find(Member.class, 101L); // DB 조회
Member findMember2 = em.find(Member.class, 101L); // 1차 캐시 조회 -> select 문 출력 x
System.out.println("result = " + (findMember1==findMember2)); // true
// 동일(같은 트랜잭션에서 실행시) -> 자바 collection과 같은 결과
1차 캐시가 존재하기 때문에 findMember1과 findMember2가 동일하다는 결과를 출력될 수 있다.
이때 동일한 트랜잭션에서 실행되어야 같다는 결과를 얻을 수 있다.
트랜잭션을 지원하는 쓰기 지원
Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");
em.persist(member1);
em.persist(member2);
System.out.println("==================");
// 밑줄이 먼저 출력되고 insert 쿼리가 2번 실행됨
// -> SQL 저장소에 쿼리를 저장하고 있다가 commit이 되는 순간 DB로 쿼리를 날린다.
// 저장소에 저장할 수 있는 제한은 persistence.xml에서 하이버네이트 옵션 속성을 통해 알 수 있다.(batch_size)
// 일종의 버퍼링 기능
transaction.commit();
엔티티 매니저에 의해 영속성이 보장이 되면 영속 컨텍스트의 1차 캐시에 데이터가 저장됨과 동시에 쓰기 지연 SQL 저장소에 insert 쿼리가 생성, 저장된다.
그리고 해당 쿼리는 SQL 저장소에 계속 저장되다 트랜잭션이 commit되는 순간, DB에 쿼리를 전송한다.
이때 저장소에 저장할 수 있는 쿼리의 갯수는 각 DB에 따라 지정할 수 있다.
// 예시
<property name="hibernate.jdbc.batch_size" value="10"/>
엔티티 수정(변경감지)
Member member = em.find(Member.class, 150L);
member.setName("ZZZZZ");
System.out.println("==================================");
update 쿼리를 작성하지 않고도 데이터가 업데이트 될 수 있는 것은 JPA가 데이터의 변경을 감지하여 자동으로 데이터를 갱신해주기 때문이다. (Dirty Checking)
트랜잭션이 commit 될 때 내부적으로 flush가 호출된다.
이때 JPA는 트랜잭션으로 들어온 객체와 기존 1차 캐시의 스냅샷에 저장되어 있는 동일한 객체를 비교한다.
만약 들어온 객체와 스냅샷에 저장되어 있는 객체의 데이터가 다르다면
update 쿼리를 SQL 저장소에 저장 후 commit 시점에 JPA가 데이터를 변경하고
변경된 객체를 다시 스냅샷으로 저장한다.
플러쉬(flush)
플러쉬는 영속성 컨텍스트의 변경 내용을 트랜잭션이 commit 되는 시점이 아닌
flush가 되는 시점에 DB에 반영한다.
플러쉬의 특징은 다음과 같다.
- 영속성 컨텍스트를 비우지 않음
- 영속성 컨텍스트의 변경 내용을 DB에 동기화
- 트랜잭션이라는 작업 단위가 중요하기 때문에 커밋 직전에만 동기화하면 됨
✨ 플러쉬 발생 시점
1) 변경 감지
2) 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
3) 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송(등록, 수정, 삭제 쿼리)
✨ 영속성 컨텍스트를 flush하는 방법
1) em.flush → 직접 호출
2) 트랜잭션 커밋
3) JPQL 쿼리 실행
1번을 제외한 2번과 3번은 flush가 자동으로 호출된다.
em.flush가 직접 호출될 때는 아래와 같이 작성될 수 있다.
// 플러쉬
Member member = new Member(201L, "member200");
em.persist(member);
em.flush();
// 원래는 쿼리가 트랜잭션이 commit 될 때 실행되어 출력되지만
// flush를 사용하면 이것을 강제로 이 시점에 발생하도록 만든다.
// 1차 캐시는 여전히 남아있다.
// 영속성 context 안에 저장되어 한 번에 DB에 저장되던 것이 즉시 DB에 반영된다.
System.out.println("=========================");
원래의 insert 쿼리는 트랜잭션이 commit 되는 시점에 전송되지만
em.flush()를 사용하면 flush 코드가 작성된 시점에서 데이터를 DB에 insert 하게 된다.
✨ 플러쉬 모드 옵션
플러쉬 옵션을 변경할 수는 있으나 가급적 변경하지 않는 것이 좋다.
em.setFlushMode(FlushModeType.COMMIT)
1. FlushModeType.AUTO
2. FlushModeType.COMMIT
: 커밋이나 쿼리를 실행할 때 플러시(기본값)
: 커밋할 때문 플러시
준영속성 상태
- 영속상태의 엔티티가 영속성 컨텍스트에서 분리된 상태(detached)
- 영속성 컨텍스트가 제공하는 기능 사용 불가
✨ 준영속 상태로 만드는 방법
1) em.detach(entity)
특정 엔티티만 준영속 상태로 전환
2) em.clear()
영속성 컨텍스트를 완전히 초기화
3) em.close()
영속성 컨텍스트를 종료
1) em.detach
// 준영속성
Member member = em.find(Member.class, 150L);
member.setName("AAAAAA");
em.detach(member);
// JPA가 관리하는 객체에서 제외된다. -> 준영속성
// find 하는 쿼리는 출력되지만
// update 쿼리는 출력되지 않는다. -> JPA가 관리하는 객제에서 제외되었기 때문
System.out.println("=========================");
em.detach()를 사용하면 코드가 작성된 시점에서 member 객체가 JPA의 영속성에서 제외되기 때문에
트랜잭션이 commit 될 때 update 쿼리가 DB로 전송되지 않는다.
2) em.clear()
Member member = em.find(Member.class, 150L);
member.setName("AAAAAA");
em.clear();
// 통째로 초기화 -> member 자체가 JPA가 관리하는 객체에서 제외
// 영속 컨텍스트에서 아예 삭제된다.
Member member2 = em.find(Member.class, 150L);
// 실행되면 select 쿼리가 두 번 출력됨 (select 쿼리 2번 -> =====)
// 첫 번째 select -> find에 의한 select 쿼리
// 두 번째 select -> em.clear에 의해 영속 컨텍스트에 member 객체가 아예 삭제되어 있음
// -> find를 하기 위해서 영속 컨텍스트에 member2라는 이름으로 객체를 새롭게 저장
System.out.println("=========================");
em.clear()는 영속성 컨텍스트를 완전히 초기화한다.
때문에 select 쿼리가 2번 출력된다.
- 첫 번째 select 쿼리
: member 객체의 정보를 update 하기 위해 select - 두 번째 select 쿼리
: em.clear()에 의해 영속성 컨텍스트가 초기화 된 후 Id 값이 '150L'인 member 타입의 객체를 DB에서 조회
→ 영속성 컨텍스트에 다시 저장된다.
'공부 기록 > JPA' 카테고리의 다른 글
[JPA] Entity Mapping에 대하여 3 - 필드와 컬럼 매핑 (0) | 2023.04.17 |
---|---|
[JPA] Entity Mapping에 대하여 2 - 데이터베이스 스키마 자동 생성 (0) | 2023.04.17 |
[JPA] Entity Mapping에 대하여 1 - 객체와 테이블 (0) | 2023.04.17 |
[JPA] JPA 시작하기 (1) | 2023.04.13 |
[JPA] JPA란 무엇일까 (0) | 2023.04.11 |