1. 영속성 컨텍스트
객체와 관계형 데이터베이스를 매핑(ORM)하는 것과 영속성 컨텍스트를 이해하는 것이 JPA 공부의 핵심이다. 영속성 컨텍스트를 이해하면 JPA가 내부에서 어떻게 동작하는지 이해할 수 있다.
영속성 컨텍스트란 엔티티를 영구적으로 저장하는 환경이라는 뜻이다. 예를 들어 EntitiyManager.persist(entity); 코드를 통해 엔티티를 영속화(영속성 컨텍스트에 저장)할 수 있다. 아래에서 더 자세히 알아보자.
엔티티 매니저 팩토리와 엔티티 매니저
웹 애플리케이션을 개발한다고 하면, 고객의 요청이 올 때마다 EntityManagerFactory를 통해 EntityManager를 생성해 배치한다. EntityManager는 내부적으로 데이터베이스 커넥션풀을 사용해 DB와 통신한다. 영속성 컨텍스트는 논리적인 개념이기 때문에 눈으로 직접 볼 수 없다. 따라서 EntityManager를 통해 영속성 컨텍스트에 접근해야 한다.
아래 그림을 살펴보면, J2SE 환경에서는 EntityManager가 생성될 때 영속성 컨텍스트도 일대일로 같이 생성된다. EntityManager마다 안에 눈으론 볼 수 없는 영속성 컨텍스트 공간이 생긴다고 보면 된다.
그와 다르게 J2EE나 스프링 프레임워크 같은 컨테이너 환경에서는 EntityManager와 영속성 컨텍스트가 다대일 관계를 이룬다. 즉, 모든 EntityManager가 하나의 영속성 컨텍스트를 공유한다고 보면 된다.
엔티티의 생명주기
다음으로 엔티티의 생명주기에 대해 알아보자. 엔티티는 4가지의 상태로 나뉠 수 있다. 그림으로 나타내면 아래와 같다.
- 비영속 (new/transient): 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속 (managed): 영속성 컨텍스트에 의해 관리되는 상태
- 준영속 (detached): 영속성 컨텍스트에 저장됐다가 분리된 상태
- 삭제 (removed): 삭제된 상태
a. 비영속 상태
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태를 뜻한다. 회원 객체를 생성만 해둔 상태라고 보면 된다.
b. 영속 상태
영속성 컨텍스트에 의해 관리되는 상태를 뜻한다. 새로 생성한 객체를 EntityManager를 이용해 영속화해 둔 상태라고 보면 된다. → em.persist(member);
참고
영속화된다고 해서 DB에 저장된다는 뜻은 아니다. 따라서 SQL이 실행되지 않는다.
c. 준영속 상태
영속성 컨텍스트에 저장됐다가 분리된 상태를 뜻한다. → em.detach(member);
d. 삭제 상태
삭제된 상태를 뜻한다. → em.remove(member);
영속성 컨텍스트의 이점
애플리케이션과 데이터베이스 사이에 영속성 컨텍스트라는 중간 계층이 하나 있다고 보면 된다.
a. 1차 캐시
영속성 컨텍스트는 내부에 1차 캐시라는 걸 갖고 있다. 회원 객체를 생성한 뒤 em.persist(member);를 통해 엔티티를 영속화하면, 1차 캐시에 엔티티의 PK와 객체 자체가 Key:Value처럼 저장된다.
이후 em.find(Member.class, "member1");를 실행하면, 우선 영속성 컨텍스트의 1차 캐시에서 조회한다.
1차 캐시에 엔티티가 없다면, DB에서 조회한다. 이때 조회한 엔티티를 1차 캐시에 저장하고, 새로운 객체에 포장해서 반환한다.
참고
영속성 컨텍스트의 EntityManager는 트랜잭션 단위로 관리되는데, 트랜잭션이 끝나면 1차 캐시도 함께 날아가기 때문에 큰 이점은 아니다. 그래도 비즈니스 로직이 정말 복잡한 경우엔 약간의 성능을 향상할 수 있다.
애플리케이션 전체에서 공유하는 캐시는 2차 캐시라고 부른다.
b. 동일성(identity) 보장
영속 엔티티의 동일성을 보장한다. 1차 캐시로 반복 가능한 읽기(Repeatable Read) 등급의 트랜잭션 고립(격리) 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");
System.out.println(a==b); // 동일성 비교, true
c. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
영속성 컨텍스트 내부에 쓰기 지연 SQL 저장소라는 게 존재한다. SQL이 생성될 때마다 바로 실행하지 않고, 저장소에 모아둔 뒤 트랜잭션이 커밋될 때 한 번에 실행(flush)한다. 매번 SQL을 실행하게 되면 성능이 떨어지게 된다.
참고
hibernate.jdbc.batch.size를 조절하면 저장소의 크기를 바꿀 수 있다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 DB에 보내지 않는다.
transaction.commit(); // 트랜잭션 커밋
// flush = 트랜잭션을 커밋하는 순간 DB에 INSERT SQL을 보낸다.
d. 변경 감지(Dirty Checking)
엔티티를 수정하더라도 em.persist(member);나 em.update(member); 같은 코드를 입력할 필요가 없다. 영속성 컨텍스트에서 조회한 엔티티의 값을 바꾸면 트랜잭션이 커밋되는 시점에 엔티티의 변경을 감지하고 알아서 UPDATE SQL을 실행해 준다. 자바 컬렉션에서 하는 것처럼 편하게 코드를 작성하기만 하면 된다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
Member memberA = em.find(Member.class, "memberA");
memberA.setUsername("hi");
memberA.setAge(10);
// em.persist(memberA)나 em.update(memberA) 같은 코드가 있어야 하지 않을까?
transaction.commit(); // 트랜잭션 커밋
변경 감지(Dirty Checking) 과정에 대해 알아보자. 트랜잭션 커밋 시점에 아래와 같은 일이 일어난다.
1. flush()를 호출한다.
2. 1차 캐시에 있는 엔티티와 스냅샷을 비교한다. 여기서 스냅샷은 값을 읽어온 최초 시점의 값을 말한다.
3. 변경을 감지하면, UPDATE SQL을 쓰기 지연 SQL 저장소에 넣어둔다.
4. 쓰기 지연 SQL 저장소에서 모아둔 SQL을 실행한다.
5. 트랜잭션을 커밋한다.
e. 지연 로딩(Lazy Loading)
이 부분은 [프록시와 연관관계 관리] 섹션에서 설명한다.
2. 플러시
플러시는 영속성 컨텍스트의 변경 내용(SQL 문)을 DB에 반영(동기화)한다는 것을 뜻한다. 트랜잭션이 커밋되면 플러시가 자동으로 발생한다. 플러시가 발생하면 어떤 일이 생길까?
- 위에서 말한 변경 감지(Dirty Checking)가 발생하고, 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록한다. 아후 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다. 이후 트랜잭션이 커밋된다.
- 플러시를 해도 영속성 컨텍스트와 1차 캐시가 날아가진 않는다. 위에 말한 것들이 DB에 반영되는 것뿐이다.
- 트랜잭션이라는 작업 단위가 중요하기 때문에 커밋 직전에만 동기화하면 된다.
영속성 컨텍스트 플러시
직접 쓸 일은 거의 없지만, 테스트할 때 쓸 수도 있기 때문에 알아두면 좋다.
- em.flush(): 직접 호출
- 트랜잭션 커밋: 플러시 자동 호출
- JPQL 쿼리 실행: 플러시 자동 호출
참고
JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유는?
아래 예시를 보면, memberA, B, C를 영속화시킨 뒤 SQL은 쓰기 지연된 상태다. JPQL을 실행했는데 DB에 가져올 값이 없어서 문제가 생길 수도 있기 때문에 JPQL은 플러시를 호출하고 쿼리를 실행하는 걸 기본값으로 해뒀다.
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
참고
플러시 모드 옵션이 존재한다. 직접 쓸 일은 거의 없으며, 기본값은 AUTO다.
- FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시
- FlushModeType.COMMIT: 커밋할 때만 플러시
3. 준영속 상태
엔티티가 영속 상태에서 준영속 상태로 변할 수 있는 경우는 크게 아래 2가지가 있다. 영속 상태는 1차 캐시에 저장된 상태라고 보면 된다.
// New to Managed
em.persist(entity);
// Detached to Managed
em.merge(entity);
준영속 상태란 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것을 뜻한다. 분리된 상태기 때문에 영속성 컨텍스트에서 제공하는 기능(변경 감지 등)을 사용할 수 없다. 영속 상태에서 준영속 상태로 만드는 방법은 아래 3가지가 있다.
- em.detach(entity): 특정 엔티티만 준영속 상태로 전환
- em.clear(): 영속성 컨텍스트를 완전히 초기화
- em.close(): 영속성 컨텍스트를 종료