그간 Spring Data JPA를 사용하면서, JPA의 정확한 흐름을 모르고 있다는 생각이 들어 기초부터 다시 공부하고 있다. 책은 김영한 님의 자바 ORM 표준 JPA 프로그래밍을 참고하였다.
하나의 작업을 위한 일꾼, EntityManager
순수 JPA에서는 어떠한 작업을 하기 위해, EntityManagerFactory, EntityManager라는 것이 존재한다.
- EntityManagerFactory
엔티티 매니저를 만들어낸다. 해당 객체를 만드는데 상당한 비용이 소모되므로, 애플리케이션 전체에서 한 개만 만들어 공유하며 사용한다.
JPA는 EntityManagerFactory를 생성할 때, Connection Pool 도 생성하는데 이게 가능한 이유는 persistence.xml에 DB 정보를 미리 적어놓고 EntityManagerFactory는 해당 xml을 참조해서 객체를 생성하기 때문에 가능하다.
- EntityManager
엔티티를 관리한다. 엔티티 매니저 팩토리는 엔티티 매니저를 생성해두지만, 트랜잭션을 시작하기 전까지 커넥션을 얻지 않은 상태로 대기한다. 트랜잭션이 시작되면 DB 커넥션을 얻는다.
영속성 컨텍스트
영속성 컨텍스트는 '엔티티를 영구 저장하는 환경' 이라 보면 된다.
우리가 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지며, 엔티티 매니저를 통해 영속성 컨텍스트에 접근하고, 관리할 수 있다.
- 엔티티의 Life Cycle
엔티티에는 4가지 상태가 존재하는데, 영속성 컨텍스트 내에 어떻게 자리하느냐에 따라 다르다.
- 비영속 - 영속성 컨텍스트와 관계가 없는 상태
- 영속 - 영속성 컨텍스트에 저장된 상태
- 준영속 - 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 - 삭제된 상태
비영속
Member member = new Member();
member.setId("member1");
....
객체를 생성했지만 영속화되지 않은 상태이다. 데이터 베이스와 전혀 관련이 없다.
영속
em.persist(member);
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장했다. 위처럼 영속성 컨텍스트가 관리되는 엔티티를 영속 상태라고 한다. 추가적으로 em.find(), JPQL을 사용하여 조회된 엔티티도 영속 상태로 영속성 컨텍스트가 관리한다.
준영속
em.detach(member);
영속성 컨텍스트가 영속 상태의 엔티티를 관리하지 않게되면 준영속 상태가 된다. em.detach(), em.close(), em.clear() 는 엔티티를 준영속 상태로 만든다.
삭제
em.remove(member);
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
- 영속성 컨텍스트의 특징
식별자 값 (Id)
영속성 컨텍스트는 엔티티를 식별자값 (@Id로 테이블의 기본키와 매핑한 값) 으로 구분한다. 식별자 값이 없으면 예외가 발생한다.
1차 캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있는데, 영속 상태의 엔티티는 모두 이곳에 저장된다.
구조는 내부에 Map이 있는데 키는 @Id로 매핑한 식별자고, 값은 엔티티 인스턴스이다.
데이터를 조회할 때 먼저 1차 캐시에 해당 엔티티가 있는지 조회하고, 없다면 DB에서 엔티티를 조회한다. DB에 데이터가 있다면 조회한 엔티티를 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");
System.out.println(a == b); // 동일성 비교. 결과는 참
식별자가 같은 엔티티 인스턴스를 조회해서 비교해보면, 같은 인스턴스임을 확인할 수 있다. 이처럼, 영속성 컨텍스트는 엔티티의 동일성을 보장한다.
- 엔티티 등록
EntityManager em = emf.createEntityManager();
EntityTransaction ts = em.getTransaction();
ts.begin();
// 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 엔티티를 영속화 했으나, SQL을 DB에 보내지는 않았다.
// 커밋하는 순간 DB에 SQL을 보낸다.
ts.commit(); // 트랜잭션 커밋
엔티티 매니저는 트랜잭션 커밋 전까지 DB에 엔티티를 저장하지 않고, 내부 쿼리 저장소에 SQL은 모아둔다. 그리고 커밋할 때 모아둔 쿼리를 한번에 내보낸다. 이를 쓰기 지연 이라고 한다.
- 엔티티 수정
JPA에는 따로 update() 라는 메소드가 존재하지 않는다. JPA에서는 대상 엔티티를 조회하여, 해당 엔티티를 변경만 해주면 엔티티의 변경사항을 감지하여 commit 시에 자동으로 update문을 DB에 보낸다. 이러한 기능을 변경 감지라 한다.
참고로, commit에는 flush가 포함되어 있는데, flush는 엔티티의 변경사항을 DB에 반영하여 동기화하는 작업이다. 이 때 등록, 수정, 삭제한 엔티티를 DB에 반영하게 된다.
EntityManager em = emf.createEntityManager();
EntityTransaction ts = em.getTransaction();
ts.begin();
// 트랜잭션 시작
Member memberA = em.find(Member.class, "memberA");
// 엔티티 수정
memberA.setUsername("hi");
ts.commit(); // 트랜잭션 커밋
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 스냅샷으로 저장해둔다. commit 시에 flush()가 먼저 호출되는데, 이 때 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다. 변경된 엔티티가 있다면 수정 쿼리를 생성해서 SQL 저장소로 보내고, 저장소의 SQL을 DB로 보낸다.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용되며, 변경된 엔티티의 필드만큼만 Update 문을 작성하는 것이 아니라, 전체 필드에 대해서 업데이트를 진행한다.
필드가 너무 방대해서 저장되는 내용이 크다면, 수정된 데이터만 사용하여 동적으로 쿼리를 생성할 수 있도록 @DynamicUpdate, @DynamicInsert 등을 사용할 수 있다.
- 플러시(flush)
위에서 언급한 것처럼, 플러시는 영속성 컨텍스트의 변경 사항을 DB에 반영하여 동기화한다.
영속성 컨텍스트를 플러시하는 방법은 3가지가 있다.
- em.flush()를 직접 호출
- 트랜잭션 commit() 시, 자동 호출
- JPQL 쿼리 실행 시, 자동 호출
flush()를 직접 호출하는 경우는 많지 않다고 한다.
- 준영속
영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라 한다. 준영속 상태의 엔티티는 영속성 컨텍스트의 기능을 사용할 수 없다.
엔티티를 준영속화 하는 방법은 총 3가지이다.
- em.detach(entity) - 특정 엔티티 준영속화
- em.clear() - 영속성 컨텍스트 초기화
- em.close() - 영속성 컨텍스트 종료
준영속화가 되면, 영속성 컨텍스트 내 1차 캐시에 저장된 모든 값들이 삭제된다.
반대로, 준영속 상태의 엔티티를 영속화하는 방법도 있다.
- em.merge(member) - 준영속 상태의 엔티티 영속화
실행과정은 다음과 같다.
1. merge() 실행
2. 준영속 상태의 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회
2-1. 만약 1차 캐시에 엔티티가 없으면 데이터 베이스에서 엔티티를 조회하여 1차캐시에 저장
3. 조회한 영속 엔티티(새로운 엔티티)에 인자로 받아온 member 엔티티의 값을 채워 넣는다.
4. member를 반환한다.
엄밀히 말하면 준영속 상태의 엔티티를 다시 영속화 하는 것이 아니라, 새로운 엔티티 객체를 만들어 준영속 상태의 엔티티의 값들을 그대로 주입하여 리턴하는 것이다.
2-1 내용처럼, 엔티티가 없더라도 새로운 Entity를 생성하여 리턴한다. 따라서 merge()는 save or update 기능을 수행한다고 볼 수 있다.
'JPA' 카테고리의 다른 글
QueryDSL 적용기 (1) | 2023.05.20 |
---|