JPA part 3
Persistence Management
JPA에서 제공해주는 Entitiy Manger을 이용해서 Entity에 대한 CRUD을 처리할 수 있다.개발자 입장에서는 Entity Manager을 작은 가상의 DB라고 생각하면 된다.
Entity Manager
persistence.xml에 설정한 정보를 바탕으로 Entity Manager Factory 객체를 생성한다.
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
위와 같이 생성한 Entity Manager Factory를 이용해서 Entity Manager을 생성받아서 사용하게 된다.
//엔티티 매니저 생성
EntityManager em=emf.createEntityManager();
위의 그림을 보면 entity manager factory는 application 전반에 걸쳐 공유되며, entity manager는 필요할 때마다 생성되어서 사용된다.
entity manager을 통해 DB로부터 정보를 받아올 수 있는데, 이때 connection이 entity manager에 할당된다. 따라서 여러 쓰레드간에 공유하게 되면 Concurrency 문제가 발생할 수 있기 때문에, entity manager은 반드시 쓰레드 한개에 대해서 실행되어야 한다. 또한, Entity Manager Factory를 생성하게 되면 기본적으로 connection pool도 같이 생성하여 entity manager은 필요할 때마다 connection을 할당받아 DB에 접근한다.
Persistence Context
entity manager을 이용해서 엔티티를 저장하거나 조회하면 entity manager은 영속성 컨텍스트에 엔티티를 보관한다.
즉,
em.persist(member);
해당 코드를 통해 member 엔티티를 영속성 컨텍스트에 저장하는 것이다. 이러한 영속성 컨텍스트는 entity manager을 생성할 때 하나 만들어지게 된다.
Entity Lifecycle
엔티티는 위와 같은 생명주기를 갖게 된다.
lifecylce | description |
---|---|
비영속(new/transient) | 영속성 컨텍스트와 관련 없는 상태 |
영속(managed) | 영속성 컨텍스트에 저장된 상태 |
준영속(detached) | 영속성 컨텍스트에 저장되었다가 분리된 상태 |
삭제(removed | 삭제된 상태 |
비영속 상태
엔티티를 처음 생성하게 되면, 영속성 컨텍스트와 관련 없는 순수 객체 상태이다.
Member member=new Member();
member.setId("member1");
member.setUsername("회원1");
영속 상태
이런 엔티티에 대해 persist() 나 find() 메소드를 호출하게 되면 영속성 컨텍스트에 저장되어 영속성 컨텍스트에 의해 관리된다.
em.persist(member);
준영속 상태
영속성 컨텍스트에서 관리되던 엔티티가 영속성 컨텍스트에서 분리되면 준영속 상태가 된다. em.detach()를 통해 엔티티가 분리되며, 엔티티 매니저를 닫는 em.close()나 엔티티 매니저를 초기화하는 em.clear() 메소드를 통해서도 준영속상태가 된다.
em.detach(member);
삭제
영속성 컨텍스트와 DB로부터 엔티티를 삭제한다.
em.remove(member)
Persistence Context의 특징
-
영속성 컨텍스트에 저장하기 위해서는 엔티티에 식별자값이 존재해야한다.
-
영속성 컨텍스트에 있는 엔티티들은 해당 트랜잭션이 커밋되게 되면 DB로 저장되는데 이를 flush 한다고 한다.
-
장점
- 1차 캐시
- 동일성 보장
- 쓰기 지연
- 변경 감지
- 지연 로딩
1차 캐시
영속성 컨텍스트 내부에는 1차 캐시를 유지해서 엔티티에 대한 정보를 저장한다.
// 엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 엔티티 영속
em.persist(member);
위와 같이 엔티티를 영속성 컨텍스트에 저장하게 되면 아래와 같이 1차 캐시에 엔티티를 보관하게 된다.
이제 영속성 컨텍스트에 있는 엔티티에 대해 조회를 해보자
Member member=em.find(Member.class,"member1");
다음과 같이 엔티티 타입에 대해 식별자값을 이용해서 엔티티를 조회하게 된다. 이때, 엔티티 매니저는 우선 영속성 컨텍스트에서 엔티티를 검색하게 된다. 만약 없으면 DB에서 조회를 수행한다.
Member member2=em.find(Member.class,"member2");
만약 위와 같이 영속성 컨텍스트에 없는 객체에 대한 탐색을 수행하면 어떻게 될까? 아래와 같이 DB로 부터 엔티티를 검색해서 영속성 컨텍스트 1차 캐시에 해당 정보를 추가하고, 이 정보를 반환한다. 즉, find을 통해서도 엔티티가 영속 상태가 되는 것을 확인 할 수 있다.
동일성 보장
영속성 컨테스트는 1차 캐시에 있는 엔티티 정보를 바탕으로 엔티티 정보를 반환한다.
따라서, 아래의 인스턴스들은 같은 인스턴스들을 참조하게 된다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교
쓰기 지연
영속성 컨텍스트는 DB에 매 순간 SQL을 수행하는 것이 아니라 트랜잭션 단위로 수행하게 된다. 그래서 INSERT 문 같은 경우도 트랜잭션 단위로 일괄 처리를 진행해 성능을 높일 수 있다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
위와 같이, 2개의 객체에 대한 엔티티를 저장하게 되면 우선적으로 영속성 컨텍스트에 저장되게 된다. 또한, 해당 인스턴스에 대한 INSERT SQL는 따로 보관해놓고, transaction이 커밋되면 그때 DB에 실행해서 엔티티들을 DB에 저장하게 된다. 이를 통해 지연 쓰기를 지원한다.
엔티티 수정
매 순간순간 엔티티를 수정하기 위해서는 기존에는 SQL문을 작성해서 진행해야 했다. 또한, 한번에 이름만 수정하기도 하고, 이름, 나이를 수정하기도 하는 등, 수정할 수 있는 옵션들이 너무 많기 때문에 이에 대해서 모든 경우의 SQL 문을 설계하는 것은 꽤나 복잡하다.
영속성 컨텍스트에서는 이러한 엔티티에 대한 변경을 감지해서 자동으로 DB에 적용시켜준다. 영속성 컨텍스트에서는 엔티티를 저장할때 스냅샷이라는 것을 통해 처음 인스턴스의 상태를 저장하게 된다. 이 스냅샷을 통해 변경된 인스턴스를 찾아 UPDATE SQL문을 생성해 DB에 적용한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
Member member=em.find(Member.class,"member1");
//인스턴스의 값을 수정한다.
member.setUsername("hello");
member.setAge(10);
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
커밋이 호출 되게 되면 영속성 컨텍스트에서는 스냅샷을 이용해서 변경된 엔티티를 찾아 해당 엔티티에 대한 UPDATE SQL문을 작성해서 DB에 적용한다.
jpa가 수정하는 기본전략은 모든 필드에 대한 UPDATE SQL문을 구성하는 것이다.
만약 이름만 수정하면 아래와 같은 SQL문이 실행될 것 같지만
UPDATE MEMBER
SET NAME=?
WHERE ID=?
실은 아래의 sql문이 실행된다.
UPDATE MEMBER
SET
NAME=?
AGE=?
WHERE ID=?
모든 필드를 이용한 SQL문 설계를 통해 모든 수정문에 대해 재사용이 가능하다는 장점이 있다.
하지만, 컬림이 많은 경우에 대해서는 동적으로 UPDATE sql문을 구성하도록 설정할 수 있다.
@Entity
@org.hibernate.annotations.DynamicUpdate
@Table(name="Member")
public class Member{
...
}
위와 같이 org.hibernate.annotations.DynamicUpdate 를 명시해주면 된다. 하지만 컬럼이 30개 이상이 넘어가지 않는 이상 JPA의 기본전략의 성능이 더 우세하다.
엔티티 삭제 엔티티 삭제를 실행하게 되면 우선 영속성 컨텍스트에서 엔티티가 삭제되고 나중에 커밋될떄 DB에도 삭제가 반영된다.
em.remove(member);
플러시
영속성 컨텍스트의 변경내용을 DB에 반영하는 것을 플러시라고 한다.
변경 감지가 수행된 영속성 컨텍스트에서는 스냅샷을 이용해서 변경된 엔티티를 검색하게 되고, 변경된 엔티티에 대해서는 SQL문을 작성해서 쓰기 지연 SQL 저장소에 저장하게 되고, 나중에 해당 SQL문들을 DB에 전송하게 된다.
플러시가 수행되는 경우는 아래의 3가지 경우이다
- em.flush
- transaction이 커밋될 때
- JPQL 실행시
em.flush
엔티티 매니저의 플러시 메소드를 이용해서 플러시를 직접 수행한다.
transaction commit
트랜잭션이 커밋될 때 자동적으로 영속성 컨텍스트에 대한 플러시 기능을 수행해 변경사항이 DB에 저장되게 된다.
JPQL 실행
JPQL을 실행하게 되면 SQL문으로 변환되어 바로 DB에 실행되기 때문에 기존의 영속성 컨텍스트에 대한 정보들이 DB에 반영된 상태에서 수행되어야 한다. 따라서, jpql 실행전에 영속성 컨텍스트에 대한 플러시가 수행된다. em.find()의 조회 기능과는 다르게 작동한다.
Flush mode
엔티티 매니저에 대한 플러시 모드를 설정할 수 있다.
mode | description |
---|---|
FlushModeType.AUTO | 커밋이나 쿼리를 실행할 때 플러시(기본값) |
FlushModeType.COMMIT | 커밋할 때만 플러시 |
아래의 setFlushMode 메소드를 이용해서 flush 모드를 변경할 수 있다.
em.setFlushMode(FlushModeType.COMMIT);
준영속
엔티티를 영속성 컨텍스트에서 분리하게 되면, 해당 엔티티는 준영속 상태가 되며, 영속성 컨텍스트에서 제공하는 기능을 수행할 수 없게 된다. 아래의 3가지 경우를 통해 준영속 상태로 만들 수 있다.
em.detach()
em.detach(member);
해당 엔티티를 준영속 상태로 만들게 되면,
위의 그림의 과정과 같이, 1차 캐시에서 엔티티 정보가 삭제되며, 지연 쓰기 SQL저장소에서의 해당 엔티티 관련 sql들이 삭제된다.
em.clear()
em.detach() 하나의 엔티티에 대해 영속성 컨텍스트에서 분리되었다면, clear는 모든 엔티티를 준영속 상태로 만드는 것이다. 영속성 컨텍스트에서 분리된 엔티티들에 대해서는 영속성 컨텍스트가 제공하는 기능들을 사용할 수 없게 된다.
em.clear();
em.close()
엔티티 매니저가 종료되면 자동적으로 내부에 있는 영속성 컨텍스트는 제거되게 된다.
em.close();
준영속 상태인 엔티티들이 가지는 특징
-
비영속 상태와 다름이 없다. 우선, 영속성 컨텍스트가 제공해주는 1차 캐시, 쓰기 지연, 변경 감지 들의 기능을 사용할 수 없다.
-
id값을 가지고 있다. 한 번 영속 상태가 된 엔티티는 식별자값을 가지고 있다.
-
지연로딩을 수행할 수 없다. JPA에서는 객체가 실제 존재하지 않는 시점에 사용자가 해당 객체를 호출하는 경우 프록시 객체를 제공하여, 실제 해당 객체를 사용하는 시점에 실제 객체를 반환하는 지연로딩을 제공하지만, 준영속 상태인 엔티티에 대해서는 지연로딩을 수행할 수 없다.
merge
준영속 상태인 엔티티를 merge를 통해 다시 영속 상태로 만들 수 있다.
static Member createMember(String id, String username){
EntityManager em= emf.createEntityManager();
EntityTransaction tx=em.getTransaction();
tx.begin();
//비즈니스 로직 수행
Member member=new Member();
member.setId(id);
member.setUsername(username);
//멤버를 영속성 컨텍스트에 저장
em.persist(member);
tx.commit();
//멤버는 준영속 상태가 된다.
em.close();
return member;
}
//해당 엔티티는 createMember 메소드를 통해 영속 엔티티가 되었다가 엔티티 매니저가 종료되면서 준영속 상태가 되었다.
Member member=createMember("memberA","회원1");
//준영속 상태인 엔티티에 변경을 수행해도 DB에 적용되지 않는다.
member.setUsername("회원");
mergeMember(member);
static void mergeMember(EntityManagerFactory emf,Member member){
EntityManager em= emf.createEntityManager();
EntityTransaction tx=em.getTransaction();
tx.begin();
//비즈니스 로직 수행
//병합을 통해 해당 준영속 엔티티를 영속 상태로 다시 만든다.
Member mergedMember=em.merge(member);
tx.commit();
//준영속 상태의 엔티티
System.out.println("member username:"+ member.getUsername());
//영속 상태의 엔티티 영속 상태가 되면서 수정된 엔티티의 정보가 DB에 반영된다.
System.out.println("mergeMember username:"+mergedMember.getUsername());
//멤버는 준영속 상태가 된다.
em.close();
}
merge는 아래와 같은 방식으로 동작하게 된다.
준영속 상태의 엔티티를 병합하는 과정에서 식별자값을 이용해서 엔티티를 찾게 되는데, 준영속 상태로 되면서 1차 캐시에는 해당 엔티티에 대한 정보가 남지 않아, DB를 통해 검색해서 해당 엔티티를 다시 1차 캐시로 저장하면서 영속상태로 만들게 된다.
영속상태로 저장하게 되면서, 엔팉티의 변경을 감지해서 새로운 엔티티 정보를 영속성 컨텍스트에 저장한다.
추가로, 병합은 비영속 엔티티에 대해 적용할 수 있다. 식별자 값을 이용해서 1차 캐시, DB 모두에서 해당 엔티티를 조회 할 수 없으면 새로운 인스턴스를 생성해서 영속성 컨텍스트에 저장해 영속 엔티티 상태로 만들 수 있다.
em.contains()
contains 메소드를 이용해서 엔티티가 영속성 컨텍스트에 속해있는 지 확인할 수 있다.
em.contains(member)
References
book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저
댓글남기기