JPA는 자바 ORM 기술에 대한 API 표준 명세이며, 쉽게 말하자면 인터페이스입니다. JPA를 구현한 다양한 ORM 프레임워크가 있는데, 주로 하이버네이트를 사용합니다.
반복적인 SQL을 개발자가 직접 작성하지 않아도 되므로 생산적인 측면에서 장점이 있습니다.
또 한 mybatis나 jdbc template와(sql 매퍼) 와는 다르게 sql에 의존적이지 않습니다.
개발자가 설정한 데이터 베이스 벤더에 따라 JPA가 SQL을 만들어주기 때문에 JPA는 특정 기술에 종속되지 않습니다.
엔티티 매니저는 엔티티를 CRUD하는 엔티티에 관련된 일을 처리하는엔티티를 저장하는 가상의 데이터베이스라고 생각하자.
엔티티 매니저 팩토리는 엔티티 매니저를 만드는 공장인데, 만드는 비용이 상당히 크므로 애플리케이션 전체에서 1개만 만들어 공유합니다.
엔티티를 영구 저장하는 환경 엔티티 매니저를 사용해서 영속성 컨텍스트에 저장합니다.
영속 상태
em.persist(member);
영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 합니다. 즉 영속상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻입니다.
영속성 컨텍스트에 엔티티가 저장된다고 해서 데이터베이스에 저장되는 것은 아닙니다. Commit 되는 시점에 데이터베이스에 반영됩니다. 이것을 flush 라고합니다.
영속성 컨텍스트의 장점
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 합니다. 영속 상태의 엔티티는 모두 이곳에 저장됩니다.
em.find()를 호출하면 우선 1차 캐시에서 식별자 값으로 엔티티를 찾습니다.
찾고자 하는 엔티티가 있으면 데이터베이스를 조회하지 않고 메모리에 있는 1차캐시에서 엔티티를 찾습니다. 만약 em.find()를 호출했는데 엔티티가 1차 캐시에 없으면 앤티티 매니저는 데이터베이스를 조회해 엔티티를 생성합니다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환합니다.
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 insert sql 문을 모아둡니다. 그리고 트랜잭션이 커밋 될 때. 데이터베이스 쿼리를 보냅니다 이것을 쓰기 지연이라고 합니다.
플러시flush()는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다.
JPA는 기본적으로 데이터를 맞추거나 동시성에 관련된 것들은 데이터베이스 트랜잭션(작업의 단위)에 위임합니다.
다른 두 가지 방법은 밑에서 설명하겠습니다.
Member member = new Member(200L, "A");
entityManager.persist(member);
entityManager.flush();
tx.commit();
플러시가 일어난다고해서 1차캐시가 모두 사라지는 것은 아닙니다. 플러시는 결국
쓰기 지연 SQL 저장소에 있는 Query들이 DB에 전송되는 동기화 과정입니다.
트랜젝션 종료시 1차캐시 비워집니다.
중요한것은 영속성 컨텍스트의 내용을 비우거나 지우는 것이 아닌 플러시는 영속성 컨텍스트의 변경 내용을 DB에 동기화한다고 보면됩니다.
JPA로 엔티티를 수정할 때 엔티티의 변경 사항을 데이터베이스에 자동으로 반영하는 기능을 변경감지라 말합니다.(이 떄 변화의 기준은 기준은 최초 조회 상태입니다.) 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됩니다.
엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해 두는데 이것을 스냅샷이라 합니다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾습니다.
엔티티가 수정 됐을 때를 요약하자면
1. 트랜잭션을 커밋하면 앤티티 매니저 내부에서 먼저 플러시가 호출된다.
자세한 플러시을 동작과정은 밑과 같습니다.
2. 앤티티와 스냅샷을 비교해서 변경된 앤티티를 찾는다.
3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL저장소에 보낸다.
4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
5. 데이터베이스 트랜잭션을 커밋한다.
이러한 과정 때문에 플러시 작업은 정상적인 조건에서 트랜잭션이 커밋되면 백그라운드에서 자동으로 실행됩니다. 일반적으로 PersistenceContext에서 수동 플러시를 실행할 필요가 없습니다.
기억해야 할 것은 변경 사항은 트랜잭션이 커밋된 경우에만 영구적이 될 수 있습니다.
Jpa는
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();
쿼리를 실행시 플러시가 되지않는다면 commit 데이터베이스에 없으므로 쿼리 결과로 조회되지 않을것입니다. member들이 모두 영속 상태라면 JPA는 이런 문제를 예방하기 위해 JPQL을 실행할 때도 플러시를 자동 호출합니다.
JPQL은 기본적으로 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회합니다. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야합니다.
//1
product.setPrice(2000);
//2
Product prodct = em.createQuery("select p from Product p where p.price = 2000",Product,class).getStringleResult();
첫 번째 setPrice()를 호출시 영속성 컨텍스트에 1000 -> 2000원 반영이되지만 commit을 하지않아 DB에는 반영이 되지앟는다. 두 번째 JPQL쿼리를 실행시 2000원인 상품을 조회시 플러시 모드를 따로 설정하지 않으면 자동으로 AUTO일 것입니다. 이 말은 즉 쿼리 실행 직전에 영속성 컨텍스트가 플러시됩니다. 2000원 상품을 조회 할 수 있습니다. 만약 이상황에서 flush 모드를 Auto 가아닌 commit으로 설정한다면 JPQL쿼리 실행시 자동 플러시가 되지않으므로 방금 수정한 데이터를 조회할 수 없습니다. 이때는 직접 em.flush()로 수동 플러시를 해줘야 합니다.
그럼에도 Commit 모드를 쓰는 이유는 성능상에 이점이 있기 때문입니다.
만약 Auto모드 일시
등록() -> 쿼리() 자동플러시 -> 등록() -> 쿼리() 자동플러시 ......
이런 비지니스 로직에서 플러시를 여러번 호출 하는것이 아닌 한번만 호출해 성능상의 이점을 가져갈수 있습니다.
Hibernate는
Person person = new Person();
Person.setName("회원1");
em.persist(person);
em.createQuery("SELECT t FROM PersonTeam t").getResultList();
Query q = em.createQuery("SELECT p FROM Person p WHERE p.name = :name");
q.setParameter("name", "감자");
q.getResultList();
em.getTransaction().commit();
Auto 일경우 PersonTeam 엔티티가 Person엔티티를 참조하지 않기 때문에
insert 쿼리가 ("SELECT t FROM PersonTeam t").getResultList(); 쿼리를 실행하여도 나가지않습니다.
commit시점에야 비로소 insert 쿼리가 데이터베이스로 날라가게 됩니다.
하지만 ALWAYS 모드의 경우
EntityManager em = emf.createEntityManager();
Session session = em.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.ALWAYS);
PersonTEam Person의 존재를 모름에도 불구하고 플러시 모드를 ALWAYS로 세팅하였기 때문에 select 쿼리가 날라가기 전에 insert 쿼리가 수행됩니다.
쓰기 지연 저장소에 모아진 SQL이 Flush 될 때도 순서가 정해져 있습니다.
영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라 합니다.
당연히 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떤 기능도 동작하지 않습니다.
em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
em.clear() : 영속성 컨텍스트를 완전히 초기화
em.close() : 영속성 컨텍스트를 종료
엔티티를 준영속 상태로 전환 : detach()
em.detach(memberA)를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됩니다.
detach가 하나의 엔티티라면 clear는 모든 엔티티를 준영속 상태로 만듭니다.
em.clear()를 호출하는 순간 영속성 컨텍스트에 있는 모든 것이 초기화됩니다. 영속성 컨텍스트에 관리되던 모든 엔티티들이 준영속 상태가 됩니다.
영속성 컨텍스트 종료 : close()
가장 큰차이로는 식별자가 있느냐 없느냐 입니다. 비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속 상태였기 때문에 반드시 식별자 값을 가지고 있습니다.
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 merge()를 사용합니다.
준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환합니다.
//준영속 상태에서 회원의 상태를 변경할 경우
Member member - createMember("memberA", "회원1"); //준영속 상태 엔티티 반환.
memeber.setUsername("회원명 변경)";
Member mergeMember = em2.merge(member);
tx2.commit // 엔티티 트렌젝션 커밋
따라서 merge 연산 후에 원본 엔티티의 동일성은 보장되지 않습니다. 즉, merge된 엔티티와 원본 엔티티는 같은 객체로 간주되지 않습니다. 이는 merge된 엔티티가 새로운 객체로 생성되어 원본 엔티티와 다른 동일성을 가지기 때문입니다.
이 때 파라미터로 넘어온 member의 참조 변수는 여전히 준영속 상태입니다. mergeMember와는 주소값이 다른 변수로 서로 다른 인스턴스입니다.
더이상 member 변수는 쓸모없기 떄문에 안전하게 하기 위해서는
Member mergeMember = em2.merge(member); 보다는
member= em2.merge(member); 가 더 안전합니다.
@Transactional
public <S extends T> S save(S entity) {
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
entityInformation에서 새로운 entity이면 persist() 를 그게 아니면 merge()를 호출합니다.
persist -> detached -> persit = merge(새로운 상태)
Member member = new Mmeber();
Member newMember = em.merge(member);
tx.commit;
변경 감지와는 달리, 준영속 엔티티의 merge는 값을 모두 덮어씌우는 방식입니다.
병합 시 값이 없으면, null로 업데이트 할 위험이 있습니다. 변경할 필요가 없는 필드도 모두 변경합니다. 결론적으로는 변경 감지를 사용하는 것이 좋습니다.
참고자료: https://umanking.github.io/2019/04/12/jpa-persist-merge/
잘봤습니다. 좋은 글 감사합니다.