본문 바로가기

Backend/JPA

[JPA] 영속성 관리(영속성 컨텍스트, 엔티티 매니저)

728x90

개요

JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분 & 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.

이번 기록에서 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아보자.

 

엔티티 매니저가 하는 일은 엔티티에 대한 CRUD 작업 등 엔티티 관련 모든 일을 처리하는 관리자라고 할 수 있다.

엔티티 매니저 팩토리와 엔티티 매니저

엔티티 매니저 팩토리는 이름 그대로 엔티티 매니저를 만드는 공장이다. 공장을 만드는 비용은 당연히 크다.
따라서 한 개만 만들어 애플리케이션 전체에서 공유하도록 설계되어 있다. 반면 공장에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다. 

엔티티 매니저 팩토리 생성

private EntityManagerFactory emf = Persistence.createEntityManagerFactory("name");

엔티티 매니저 생성

private EntityManager em = emf.createEntityManager();

엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다.
요청이 올 때마다 엔티티 매니저를 생성하고, 엔티티 매니저는 내부적으로 데이터 커넥션을 이용해 DB에 접근한다.

 

영속성 컨텍스트

영속성 컨텍스트는 의역하면, 엔티티를 영구 저장하는 환경이라는 뜻이다.

엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

 

엔티티를 저장하는 가상의 DB로 생각하면 편하다.

엔티티 매니저는 엔티티를 저장하거나 조회할 때 해당 엔티티를 이 영속성 컨텍스트에 보관하고 관리한다.

entityManager.persist(member);

위 코드가 바로 영속성 매니저가 영속성 컨텍스트에 회원 엔티티를 저장하게 하는 코드다.

엔티티의 생명주기

엔티티가 가지는 4가지 상태를 영속성 컨텍스트와 연관 지어보자.

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태

비영속

순수한 객체 상태이며 아직 저장되기 전 상태이다. 따라서 영속성 컨텍스트나 DB와는 전혀 관련 없다.

영속

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장했다. 이렇게 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다.

JPQL을 이용해 DB에서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태다.

준영속

영속 상태로부터 분리된 상태의 엔티티를 말한다.

entityManager.detach(member);

위와 같이 분리시킬 수 있다.

entityManager.close();
entityManager.clear();

또는 위처럼 매니저를 종료시키거나 클리어하면, 관리되고 있던 엔티티가 분리된다.

삭제

엔티티를 영속성 컨텍스트와 DB에서 삭제한다.

entityManager.remove(member);

영속성 컨텍스트의 특징

식별값

영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다.

따라서 영속 상태의 엔티티는 반드시 식별자 값을 가져야 한다.

엔티티 조회

영속성 컨텍스트 내부에 가지고 있는 캐시를 1차 캐시라고 한다.

영속 상태의 엔티티가 모두 이곳에 저장되며, 도식화하면 아래와 같다.

식별자 값은 DB의 기본 키와 매핑되어 있다. 따라서 find() 메서드로 특정 데이터를 찾으려고 할 때

1차 캐시를 먼저 찾고 해당 엔티티가 저장되어 있으면, 1차 캐시에서 엔티티를 가져온다.

 

이때 1차 캐시에 엔티티가 없다면, DB에 조회해 엔티티를 생성한다.

영속 엔티티의 동일성 보장

위 그림을 보면 알 수 있듯 find() 메서드가 반환하는 엔티티 인스턴스는 항상 같은 인스턴스다.

따라서 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.

엔티티 등록

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 DB에 엔티티를 저장하지 않고 내부 쿼리 저장소에 SQL 쿼리를 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 DB에 보내는데 이를 트랜잭션을 지원하는 쓰기 지연이라고 한다.

//트랜잭션 시작
tx.begin();

Member member1 = new Member("choi");
Member member2 = new Member("kim");

entityManager.persist(member1);
entityManager.persist(member2);

//트랜잭션 커밋
tx.commit();

위 코드를 실행할 때 동작은 아래 그림과 같다.

member1을 persist 한 결과는 위와 같다. 쓰기 지연 SQL 저장소에 위와 같이 INSERT 쿼리가 저장된다.

member2를 persist 할 경우 member2에 대한 INSERT 쿼리가 추가로 쓰기 지연 SQL 저장소에 저장된다.

위 그림을 보면, commit() 메서드를 실행했을 때 flush와 commit 두 작업을 수행하는 것을 볼 수 있다.

flush는 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업이다. 이때 등록, 수정, 삭제한 엔티티를 DB에 반영한다.

따라서 flush 작업을 수행할 때 쓰기 지연 SQL 저장소에 있는 쿼리가 DB에 전송된다. commit은 DB와의 동기화를 마친 후 실제 DB 트랜잭션을 종료시키는 작업이다.

 

 엔티티 수정

SQL 수정 쿼리의 문제점

SQL을 사용하면 수정 쿼리를 직접 작성해야 한다. 프로젝트가 점점 커지면 수정 쿼리가 점점 추가된다. 이때 휴먼 에러로 값이 누락될 수 있다. 따라서 실수를 방지하기 위해 상황에 따라 쿼리를 계속해서 추가해야 한다. 결국 간접적이든 직접적으로 비즈니스 로직이 SQL에 의존하게 된다.

 

JPA를 이용하면 변경 감지(dirty checking)로 위와 같은 문제를 해결할 수 있다.

변경 감지(dirty checking)

JPA로 엔티티를 수정할 땐 수정 쿼리를 신경 쓸 필요 없이 단순히 엔티티를 조회해 데이터만 변경하면 된다. 다시 말하면, 영속성 컨텍스트에 의해 관리되는 엔티티가 변경되면 자동으로 이를 감지해 수정 쿼리가 전송된다.

이를 위해 JPA는 엔티티의 최초 상태를 복사해 스냅샷으로 보관하고, 플러시 시점에 스냅샷과 엔티티를 비교해 변경된 엔티티를 찾는다.

변경 감지에 대한 JPA의 기본 전략은 엔티티의 모든 필드를 업데이트하는 것이다. 모든 필드를 업데이트하면 어떤 장점이 있을까?

장점

  • 수정 쿼리가 항상 같다. 따라서 어플리케이션 로딩 시점 생성된 수정 쿼리를 재사용할 수 있다.
  • DB에 동일한 쿼리를 보내면 DB는 이전에 한 번 파싱 된 쿼리를 재사용할 수 있다.

단점

  • 필드가 너무 많거나 저장되는 내용이 너무 크면 수정된 데이터에 대해서만 동적으로 SQL을 생성하는 전략이 낫다.
@Entity
@DynamicUpdate
@Table
public class Member {
	...
}

위처럼 DynamicUpdate를 이용하면, 수정된 필드에 대해서만 동적으로 UPDATE SQL을 생성할 수 있다.

Member member = new Member("choi", 10);
entityManager.persist(member);
entityManager.flush();

member = entityManager.find(Member.class, 1L);
member.setName("ch");
member.setAge(20);

위 Member 엔티티는 @DynamicUpdate를 전략으로 지정했다. 전송되는 쿼리는 아래와 같다.

age와 name이 변경되었으니 스냅샷과 다를 것이고, 수정된 두 필드의 값만을 감지해서 하나의 쿼리가 전송됐다.

플러시(flush)

앞서 플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것이라고 했다.

좀 더 구체적으로 플러시 작업 수행 과정을 보자.

  1. 변경 감지가 동작해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
  2. 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.

플러시 모드 옵션

플러시의 모드에는 두 가지 옵션이 있다.

  • AUTO : 커밋이나 쿼리를 실행할 때 플러시(기본값)
  • COMMIT : 커밋할 때만 플러시

두 모드의 차이점은 쿼리를 실행할 때 플러시 여부다. 왜 쿼리를 실행할 때 플러시해야 할까?

 

만약 쿼리 실행 전에 1차 캐시에 변경 사항이 있다고 가정해 보자. 만약 1차 캐시의 변경 사항이 실행되는 쿼리와 관련 있다면, 실행되는 쿼리가 제대로 된 결과를 반환하지 않을 수 있다.

따라서 위 상황을 방지하고자 쿼리를 실행할 때 플러시 하는 것을 기본 전략으로 한다.

 

플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는 작업이 아닌

영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업이라는 것을 잊지 말자.

마무리

위 기록은 책을 2회독하면서 단순히 책 내용을 정리한 것이다.

이미 알고 있는 내용이다 보니 추가적으로 궁금한 점들이 생겼다.

  • 트랜잭션 시작, 종료, 롤백 등이 @Transactional로 어떻게 수행될까?
  • DB로의 flush, commit 등의 작업이 JPA 관점이 아닌 DB 관점에서 어떻게 동작될까?

위 궁금한 점들로부터 Spring AOP에 대한 이해와 DB 자체에 대한 이해가 부족하다는 것을 알았다.

이 책에 대한 2회독이 끝나면, 바로 공부해서 추가할 예정이다.

참고

 

자바 ORM 표준 JPA 프로그래밍 - 예스24

자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고

m.yes24.com

 

728x90