JPA에서 가장 중요한 2가지를 알아보자.
이 둘중 이번엔 영속성 컨텍스트와 JPA 내부 동작방식 매커니즘에 대해 자세히 알아보자.
EntityManagerFactory
를 미리 생성해 놓고 고객의 요청이 올 때 마다 EntityManager
을 생성한다.EntityManager
는 내부적으로 데이터 베이스 커넥션을 사용해서 DB를 사용하게 된다.JPA를 이해하는데 가장 중요한 용어다. 실제로 존재하는 개념이 아니라 추상적, 논리적 개념이여서 눈에 보이지는 않는다.
위의 EntityManager
를 통해 영속성 컨텍스트에 접근을 한다.
쉽게 말하자면, EntityManager
안에 영속성 컨텍스트라는 눈에 보이지 않는 공간이 생긴다고 이해하자.
참고
EntityManager.persist(entity);
DB에 저장한다는 뜻은 아니다. 영속성 컨텍스트를 통해서 이 entity를 영속화 한다는 뜻이다. 더 정확히 말하면 위의
persist
메소드는 DB에 저장하는 것이 아니라entity
를 영속성 컨텍스트 라는 곳에 저장한다는 뜻이다.
EntityManager
와 영속성 컨텍스트가 1:1 이다.엔티티의 생명 주기는 크게 4가지로 구분된다.
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
영속성 컨텍스트에 관리되는 상태
하나씩 알아보자.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 객체를 생성만 하고 별도로 영속성 컨텍스트에 주입하지 않았다.
// 객체 생성
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// EntityManagerFactory를 통해 EntityManager 생성
EntityManager em = emf.createEntityManager();
// EntityManager를 통해 Transaction을 얻고 시작.
em.getTransaction().begin();
// 생성한 객체를 저장한 상태(영속)
em.persist(member);
참고) DB에 저장되지 않는 것 확인
해당 코드를 실행해보자.
// 영속 System.out.println("=== BEFORE ==="); em.persist(member); System.out.println("=== AFTER ===");
<결과>
=== BEFORE === Hibernate: call next value for MEMBER_SEQ_GENERATOR === AFTER === Hibernate: /* insert hellojpa.Member */ insert into Member (name, id) values (?, ?)
===AFTER===
이 출력된 후 INSERT QUERY가 출력되는 것을 볼 수 있다.- 이를 통해
persist()
에선 DB에 저장되는 것이 아닌 영속성 컨텍스트에 저장되는 것을 알 수 있다.transaction
을commit
하는 시점에 영속성 컨텍스트에 있는 것이 DB에 쿼리로 날라가게 된다.
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
참고) 준영속 -> 영속
em.find()
를 했는데 엔티티 객체가 영속성 컨텍스트에 없고 DB에 있으면 1차 캐시에 등록이 되므로 영속 상태가 된다.
준영속 상태로 만드는 방법
1. em.datach(entity)
2. em.clear()
3. em.close();
이러한 준영속 상태는 나중에 복잡한 웹 어플리케이션을 개발할 때 자세히 알 수 있다.
//객체를 삭제한 상태(삭제)
em.remove(member);
지금까지 어려운 개념인 영속성 컨텍스트를 알아봤다. 알아보면서 굳이 이걸 왜쓰는지에 대한 의문이 들 것이다. 영속성 컨텍스트를 사용하면 많은 이점이 있기 때문이다.
영속성 컨텍스트의 이점은 크게 5가지로 볼 수 있다.
하나씩 알아보자.
여기서는 EntityManager
자체가 영속성 컨텍스트라 생각해자.
(물론 미묘한 차이는 존재하지만 일단은 이렇게 생각하자.)
내부에는 1차 캐시라는 것이 존재한다. (이 1차캐시를 영속성 컨텍스트라 이해해도 되긴한다... 아오 복잡해)
em.find()
메소드로 조회를 하면 JPA는 우선적으로 DB가 아닌 1차 캐시에서 pk값(key
)를 통해 객체를 조회한다. 그래서 캐시에 해당 값이 있으면 그 값을 그대로 가져온다.참고
코드를 통해 조회 후 출력하는 과정에서 데이터베이스의 셀렉트 쿼리가 출력되지 않음을 확인할 수 있다.
이것이 성능적인 측면에서 크게 도움은 안된다. EntitiyManager
(영속성 컨텍스트)라는 것은 고객의 요청이 올 때 마다 생성되는 것이므로 고객의 요청이 와서 비지니스 로직을 실행 후 끝나면 사라지기 때문이다. 이 때 1차 캐시또한 날아간다.
따라서, 1차 캐시는 찰나의 순간에서만 존재하기 때므로 크게 도움이 되지 않는다.
물론, 비지니스가 매우 복잡한 경우 쿼리가 줄어들어서 이점이 있다. 그러나 계속 언급하지만 크게 성능적인 면에서 도움은 주지 않는다.
성능적인 이점보다 컨셉이 주는 이점이 있다. (좀 더 객체지향적으로 코드를 작성할 수 있다거나...)
자바의 컬렉션에서 같은 참조인 객체를 2번 꺼내온다고 생각해보자. 꺼내온 두 객체의 참조는 같을 것이다.
JPA 또한 마찬가지이다. em.find()
에서 같은 pk값으로 객체를 두 번 꺼내온다고 하면 그 두개의 참조는 같다. 이를 영속성의 동일성을 보장해준다라고 한다.
즉, 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭 션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
이를 코드로 확인하면 다음과 같다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교 true
a
, b
는 서로 같은 참조임을 알 수 있다.이는 별거 아닐 수 있지만 JPA는 이러한 DB안의 값들을 자바의 컬렉션처럼 활용할 수 있게 해준다는 점에서 의미가 있다고 할 수 있다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
persist()
하면 1차 캐시에 값이 저장되는 동시에 JPA가 해당 entity
를 분석 후 INSERT QUERY를 생성 후 별도의 저장소(쓰기 지연 SQL 저장소)에 쌓아둔다. (Query를 DB에 보내진않는다.) commit()
하면 INSERT QUERY를 보내어 DB에 데이터가 저장된다. 그림으로 자세히 확인해보자.
commit()
하는 순간 쓰기 지연 SQL 저장소에 있던 모든 INSERT QUERY가 DB로 전달된다. (이를 JPA에선 flush라 한다)이를 통해 버퍼링이라는 기능을 사용할 수 잇다. persist()
를 할 때 마다 바로 DB로 값을 넘기면 뭔가 최적화할 수 있는 여지 자체가 없어진다.
참고
하이버네이트는 최대 모아둘 수 있는 데이터양을 별도로 설정할 수 있다.
중요
JPA는 기본적으로 리플렉션같은 기능이나 프록시 객체등을 내부적으로 사용하기 때문에 동적으로 객체를 생성해야 하는 경우가 존재한다.
따라서, 엔티티 객체는 디폴트 생성자가 반드시 존재해야 한다.
JPA는 DB에 접근하는 방식이 마치 자바의 컬렉션에 접근하는 방식과 유사하다. (동일성 보장에서도 확인할 수 있었다.)
우리가 자바의 컬렉션의 값을 수정하기 위해선 참조를 가져와 값을 수정만 하면 됬다. 별도로 저장하는 로직은 필요가 없었다. 이는 JPA에서 DB에 저장된 값을 바꿀때도 마찬가지다.
단순히 값을 꺼내와서 수정만 하고 별도로 저장하지 않아도 된다. (update()
메소드를 호출할 필요없다.)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member) 코드 생략 가능
transaction.commit(); // [트랜잭션] 커밋
update()
메소드를 사용하지 않아도 DB에 수정된 값이 저장된다.값이 저장되는 과정을 그림으로 살펴보자.
commit
을 하면 내부적으로 flush
가 호출된다. flush
가 호출되는 시점에 스냅샷과 엔티티를 일일히 비교했을 때 두개가 다르면 UPDATE QUERY를 쓰기 지연 SQL 저장소에 저장한다. 그 후, DB에 반영을 하고 commit 한다. 엔티티를 변경할 때 update()
메소드를 별도로 호출하지 않으면 개발자의 실수를 줄일 수 있어 효율적이다.
DB에 저장된 데이터를 완전히 삭제한다. 작동 원리는 변경 감지(Dirty Checking)과 같다.
//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제
영속성 컨텍스트의 이점을 살펴보면서 flush
를 언급했다. 대충 이것의 의미를 파악할 수 있었을 것이다.
플러시는 영속성 컨텍스트의 변경내용을 데이터 베이스에 반영한다.
(쓰기 지연 SQL 저장소에 값들을 DB에 보낸다)
플러시가 발생하면 다음과 같은 로직이 실행된다.
플러시 자동 호출 (지금까지 했던 방식)
플러시 자동 호출
참고) JPQL 쿼리 실행시 플러시가 자동 으로 호출되는 이유
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();
JPQL은 SQL로 번역이 되서 실행이된다. (즉, DB에서 가져올 것이 없다.)
따라서, 문제 발생을 방지하기 위해 플러시가 자동으로 호출이 된다.
어려운 개념이니 일단은 이정도로 간단하게만 이해하자.
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.AUTO
FlushModeType.COMMIT
거의 도움이 되지않아 쓸 일이 없다.
참고
JPA는 기본적으로 어떤 데이터를 맞추거나 동시성에 관련된 것들은 다 DB 트랜잭션에 위임해서 한다.