엔티티를 저장하는 가상의 데이터베이스로 엔티티 매니저를 만들어내는 구현체.
엔티티 매니저 팩토리를 생성하는 것은 비용이 크지만, 엔티티 매니저 팩토리에서 엔티티를 만들어내는 비용은 적다.
따라서, 한개의 공장을 만들어서 전체 어플리케이션에서 공유하도록 설계되어있다.
즉, 엔티티 매니저 팩토리는 여러 스레드가 접근해도 괜찮지만.
엔티티 매니저는 여러 스레드가 동시에 접근할 수 없다.
그럼 이제 엔티티 매니저에 대해서 알아보자.
엔티티 매니저는 엔티티 매니저 팩토리에서 만들어진다.
엔티티 매니저의 경우 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.
즉, 트랜잭션을 시작할때 커넥션을 획득한다.
위의 그림을 보면, 팩토리에서 생성된 엔티티 매니저가 있고 엔티티 매니저에서는 디비와의 커넥션이 필요할때만 커넥션을 연결하고 있다!
JPA에서 가장 핵심적인 용어로 영속성 컨텍스트가 있다. 좀 풀어말하면 엔티티를 영구 저장하는 환경
이라는 말이다.
엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(member);
// 회원 엔티티를 저장한다 X
// 엔티티 매니저를 사용해서 영속성 컨텍스트에 회원 엔티티를 저장한다 O
영속성 컨텍스트는 엔티티 매니저를 생성할때 하나 만들어지며, 엔티티 매니저를 통해 영속성 컨텍스트에 접근하고 관리할 수 있다.
여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수도 있다 !
이러한 상황은 지금 고려하지 않고, 하나의 엔티티 매니저에 하나의 영속성 컨텍스트가 있다고 생각하자.
엔티티에는 4가지 상태가 존재한다.
엔티티 객체를 막 생성한 상태로 순수한 객체 상태이며 아직 저장하지 않은 상태를 말한다.
영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없는 상태이다.
영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다.
즉, JPA를 통해서 데이터를 가져온 엔티티는 이미 영속상태에 있다고 할 수 있다.
영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다. 특정 엔티티를 준영속 상태로 만들려면 em.detach()
를 호출하면 된다.
여담이지만, 순환참조를 피하기 위해서 엔티티를 DTO에 담는 이유 또한 영속상태에서 벗어나기 위함도 있다고 생각한다.
계속 영속성 컨텍스트에 의해 관리가 되는 엔티티를 사용하면 순환참조가 지속적으로 발생하기 때문에, DTO에 옮겨담음으로서 영속성 컨텍스트에 의해 관리되는 것을 피하기 위함인 것이 아닐까?
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다
영속성 컨텍스트의 특징은 다음과 같다
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티를 식별자(@Id로 기본키와 매핑한 값)으로 구분한다.
따라서 영속상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다 !
영속성 컨텍스트와 데이터베이스 저장
영속성 컨텍스트에 엔티티를 저장하면 이 엔티티는 언제 데이터 베이스에 저장될까? JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다 (flush)
영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
지금부터 영속성 컨텍스트가 왜 필요하고 어떤 이점이 있는지 엔티티를 CRUD 하면서 그 이유를 하나씩 알아보자.
영속성 컨텍스트는 내부에 캐시를 가지고 있다 → 1차 캐시
영속상태의 엔티티 → 영속성 컨텍스트에 저장!
그러면 어떻게 저장하느냐면, 영속성 컨텍스트 내부에 Map이 있고, 거기에 @Id
로 매핑한 식별자를 키로 사용하여 엔티티 인스턴스를 저장한다.
em.persist(member)
→ 이 코드를 실행하면 1차 캐시에 엔티티를 저장한다.
엔티티는 아직 데이터베이스에 저장된 상태는 아니다.
그림과 같이 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스 기본 키 값이다.
조회하는 코드는 다음과 같다.
Member member = em.find(Member.class,"member1");
즉, find()
를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.
위의 그림이 바로 1차캐시에 찾는 엔티티가 있어서 데이터베이스까지 접근하지 않고 조회한 경우이다.
만약, 1차캐시에 찾는 엔티티가 없다면 어떻게 될까?
엔티티 매니저가 데이터베이스를 조회해서 엔티티를 생성한다. 그런다음 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.
즉,1차캐시에 저장되어있어야 사용자에게 전달된다는 소리이다.
em.find(Member.class,"member2")
를 실행한다이렇게 영속상태, 즉 1차캐시에 저장된 엔티티들은 이제 따로 지우기 전까지는 캐싱되어 데이터베이스에 접근하지 않고도 데이터를 조회할 수 있게 된다.
영속 엔티티의 동일성 보장
다음 코드를 실행하면 어떤 결과가 나올까?
Member a = em.find(Member.class,"member1"); // 1
Member b = em.find(Member.class,"member1"); // 2
System.out.print(a == b) // 3
1번 코드를 통해 불러온 a와 2번 코드를 통해 불러온 b는 1차캐시에 저장된 엔티티를 가져오기 때문에, 결과는 당연히 참이다.
영속성 컨텍스트는 성능상의 이점과 엔티티의 동일성을 보장한다는 것을 알 수 있다.
💡 동일성과 동등성은 다른 말이다!
동일성은 실제 인스턴스가 같다는 말이고, 동등성은 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같다는 것이다.
💡 JPA는 1차 캐시를 통해 반복 가능한 읽기(조회했을때, 항상 같은 값을 조회해오는 것을 보장하는 격리 수준) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 장점이 있다.
엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 등록해보자.
EntityManager em = emf.createEntityManbaer();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야함
// 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 아직 Insert SQL을 데이터베이스에 보내지 않음
trasaction.commit();
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다.
그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보낸다!
이것을 트랜잭션을 지원하는 쓰기 지연이라고 한다.
그림 3.8의 진행순서를 보자.
위의 그림도 마찬가지이다. 회원 B도 위와같은 절차를 거쳐 등록쿼리를 쓰기지연 SQL을 저장소에 보관한다.
총 2건의 등록쿼리가 저장된다.
마지막으로 트랜잭션을 커밋하면 위와같은 그림이 나온다.
일련의 과정은 다음과 같다.
이렇게 트랜잭션을 지원하는 쓰기지연이 가능한 이유가 무엇일까?
begin(); // 트랜잭션 시작
save(A);
save(B);
save(C);
commit(); // 트랜잭션 커밋
위의 로직을 두가지 경우로 생각해보자.
트랜잭션 범위 안에서 실행되기 때문에 둘의 결과는 같다.
A,B,C 모두 트랜잭션을 커밋하면 함께 저장되고 롤백하면 함께 저장되지 않는다.
어떻게든 커밋 직전에 SQL을 전달하면 된다. 이것이 트랜잭션을 지원하는 쓰기 지연이 가능한 이유다.
이 기능을 잘 활용하면 모아둔 등록 쿼리를 데이터베이스에 한번에 전달해서 성능을 최적화할 수 있다.
SQL 수정 쿼리의 문제점
프로젝트의 규모가 커진다 → 수정쿼리도 점점 많아진다
기능이 두개가 있다고 가정하자.
이 두개의 기능은 하나의 쿼리로 작성해도 되지만, 이름과 나이를 변경할 때 실수로 등급정보를 입력하지 않거나 혹은 등급을 변경하는데 실수로 이름과 나이를 입력하지 않을 수 있다.
이러한 상황을 피하기 위해 수정 쿼리를 상황에 따라 계속 추가한다.
이런 개발방식은 다음과 같은 문제를 초래할 수 있다.
변경감지
JPA는 엔티티를 어떻게 수정할까?
JPA는 변경감지(더티체킹) 기능을 통해 커밋 직전에 데이터가 변경된 내용을 자동으로 반영한다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때,최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 한다.
그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
위의 그림을 순서대로 분석해보자.
당연한 사실이지만 변경 감지는 영속성 컨텍스트가 관리하는 영속상태의 엔티티에만 적용된다.
추가적으로 엔티티 수정에 대한 쿼리를 JPA가 자동으로 만들때 JPA는 수정된 필드만 업데이트를 하는 것이아니라 모든 필드를 업데이트한다. 이는 데이터베이스에 보내는 데이터 전송량이 증가한다는 단점이 있지만. 다음과 같은 장점으로 인해 모든 필드를 업데이트한다.
만약 필드가 많거나 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성하는 전략을 선택하면 된다.
@DynamicUpdate
를 통해서 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성한다.
INSERT SQL을 동적으로 생성하는 @DynamicInsert
도 있다.
em.remove(memberA);
이러한 방식으로 엔티티를 직접 삭제할 수 있다.
엔티티를 즉시 삭제하는 것이 아니라, 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록한다. 이후 트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달한다.
참고로 remove 메서드를 호출하는 순간 memberA는 영속성 컨텍스트에서 제거된다.
엔티티 매니저 팩토리가 존재하며 여기에서 엔티티 매니저를 만든다.
엔티티 매니저 팩토리는 여러개의 스레드에서 접근해도 되지만, 엔티티 매니저는 하나의 스레드 접근만 허용한다.
엔티티 매니저를 통해 조회하면 영속성 컨텍스트라는 곳에 저장한다.
영속성 컨텍스트에 저장되는 이 엔티티라는 것에는 상태가 있는데 다음과 같다.
영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 잇다.