본 포스팅은 인프러의 JPA 기본편을 수강하고 정리하는 내용입니다.
JPA에서 가장 중요한 2가지
- 객체와 관게형 데이터베이스 매핑하기(Object Relational mapping)
- DB를 어떻게 설계하고 객체를 어떻게 설계해서 중간에 어떻게 JPA로 매핑해서 쓸건지
- 영속성 컨텍스트
- 실제 내부에서 JPA가 어떻게 동작하는지
엔티티 매니저 팩토리와 엔티티 매니저
EntityManagerFactory를 통해 고객의 요청이 올때마다 EntityManager를 생성하고 내부적으로 DB 커넥션을 사용해서 DB를 사용하게 된다.
영속성 컨텍스트
- JPA를 이해하는데 가장 중요한 용어
엔티티를 영구 저장하는 환경
이라는 뜻EntityManager.persist(entity);
- persist() 메서드는 실제로 DB에 저장한다기보단 영속성 컨텍스트에 저장한다.
엔티티 매니저? 영속성 컨텍스트?
- 영속성 컨텍스트는 논리적인 개념
- 눈에 보이지 않는다.
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
EntityManager 를 생성하면 그안에 1대1로 PersistenceContext(영속성 컨텍스트)가 생성된다.
엔티티의 생명주기
비영속 (new/transient)
- 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
영속 (managed)
- 영속성 컨텍스트에 관리되는 상태
준영속 (detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제 (removed)
- 삭제된 상태
비영속(비영속 (new/transient))
1
2
3
4
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
일반적으로 객체를 생성 후 persist()메소드를 호출하지 않은 상태를 말한다.
영속 (managed)
1
2
3
4
5
6
7
8
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member);
생성한 객체를 영속성 컨텍스트에서 관리하고 있는 상태를 말한다. 영속 상태가 된다고 바로 DB에 쿼리가 날라가는게 아니라 커밋할 때 쿼리가 날라가 실제 DB에 반영된다.
준영속 (detached)
회원 엔티티를 영속성 컨텍스트에서 분리한 준영속 상태
1
em.detach(member);
삭제 (removed)
객체를 삭제한 상태(삭제)를 말한다.
1
em.remove(member);
영속성 컨텍스트의 이점
애플리케이션과 RDB사이의 중간의 계층이다보니 버퍼링이나 캐슁을 할 수 있다.
- 1)1차 캐시
- 2)동일성(identity) 보장
- 3)트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 4)변경 감지(Dirty Checking)
- 5)지연 로딩(Lazy Loading)
1. 1차 캐시
1
2
3
4
5
6
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티를 영속
em.persist(member);
영속성 컨텍스트는 내부에 1차 캐시
라는 것을 두고 있다. @Id가 PK를 가지고 있고 , Entity가 객체 자체를 가리키게 된다.
1차 캐시에서 조회
1
2
3
4
5
6
7
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
Member 객체를 저장해놓고 조회를 하면 먼저 1차 캐시를 뒤진다. 그래서 1차 캐시에 존재하면 캐시에 있는 값을 그냥 조회해 온다. (훨씬 조회 속도가 빠르다) 만약 1차 캐시에 존재하지 않는다면 그 다음으로 DB에서 찾게 된다.
데이터베이스에서 조회
만약 member2를 조회한다하면 1차 캐시에 없기에 DB에서 조회하게 된다. 찾은 데이터를 1차 캐시에 저장 후 반환하게 된다.
Note: 사실 이게 큰 도움은 안된다. 왜냐하면 EntityManager라는 것은 DB 트랜잭션 단위로 만들고 DB트랜잭션이 끝날때마다 삭제된다. 즉, 고객의 요청이 하나 들어와서 비즈니스가 끝나면 영속성 컨텍스트를 지운다. 굉장히 복잡할땐 도움이 될 수 있겠지만 일반적으로 굉장히 짧은 시간 동안 유지하기에 큰 도움은 없다고 볼 수 있다.
2. 동일성(identity) 보장
1
2
3
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다. 꼭 같은 트랜잭션 안에서만 적용된다.
3. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
1
2
3
4
5
6
7
8
9
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
내부적으로 버퍼를 가지고 있기에 버퍼에 쭉쭉 엔티티를 쌓다가 커밋하는 순간에 DB에 보낸다.
영속성 컨텍스트는 내부적으로 1차 캐시 뿐만 아닌 쓰기 지연 SQL 저장소
가 존재한다.
MemberA를 persist()메서드를 통해 저장하면 MemberA가 1차 캐시에 들어감과 동시에 JPA가 엔티티를 분석해서 insert쿼리를 생성해서 쓰기 지연 SQL 저장소
에 쌓아둔다.
MemberB를 저장해도 위처럼 동일하게 작동한다. 그렇다면 언제 쿼리가 DB로 날라가느냐?
트랜잭션을 커밋
하는 시점에 쓰기 지연 SQL 저장소
에 있던 쿼리들이 flush되면서 쿼리가 DB로 날라간다.
4. 변경 감지(Dirty Checking)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(); // [트랜잭션] 커밋
JPA는 변경 감지라는 기능으로 entity를 변경할 수 있는 기능이 제공된다. 자바 컬렉션처럼 데이터를 조회해서 값을 바꿔도 다시 persist()를 호출해서 저장할 필요가 없다. 비밀은 영속성 컨텍스트 안에 다 있다.
JPA는 데이터 베이스 트랜잭션을 커밋하는 시점에 내부적으로 flush() 메서드가 호출된다. 그러면 엔티티와 스냅샷을 비교(1차 캐시 안에 존재)
한다. 1차 캐시는 엔티티가 처음 영속성 컨텍스트가 저장된 상태를 스냅샷에 저장한다. 비교 후 상태 값이 변경되었으면 update쿼리를 쓰기 지연 SQL 저장소
에 만들어두고 DB에 반영하고 커밋한다.
5. 엔티티 삭제
1
2
3
4
//삭제 대상 엔티티 조회 (위의 매커니즘과 동일하고 트랜잭션 커밋 시점에 delete 쿼리가 호출된다)
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제