이번 글에서는 JPA내부 구조와 영속성 관리에 대해서 끄적여보고자 한다.
JPA에서 가장 중요한 2가지
- 객체와 관계형 데이터베이스 매핑하기(어떻게 매핑 할 것인가?)
- 영속성 컨텍스트
먼저 위 두가지 경우를 이해하기 위해서는 엔티티 매니저 팩토리와 엔티티 매니저에 대해서 이해가 필요하다.
이 두개에 대해서는 이전 글에서도 언급을 해놨으니 참고하길 바란다. -> 2020/07/01 - [Dev/JPA] - JPA의 사용
1. 영속성 컨텍스트
JPA를 이해하는데 가장 중요한 용어
"엔티티를 영구 저장하는 환경"이라는 뜻
EntityManager.persist(entity);
- DB에 저장한다는 것이 아니고 엔티티를 영속성 컨텍스트라는 곳에다가 저장한다는 뜻
- EntityManager 를 통해 영속성 컨텍스트에 접근한다.
2. 엔티티의 생명주기
- 비영속(new/transient) - 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed) - 영속성 컨텍스트에 관리되는 상태
- 준영속(detached) - 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) - 삭제된 상태
3. 영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
엔티티 1차 캐시
- 영속성 컨텍스트(entityManager)에는 1차캐시라는 공간이 존재한다.
- 디비에 값을 조회(find)를 통해 하게될 경우 JPA는 바로 디비에서 찾지 않는다.
- 우선순위로 1차캐시에서 찾고 그 값을 반환해준다.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
엔티티 1차 캐시 / 데이터베이스에서 조회 할 경우
- member2를 조회해야하는데 만일 디비에는 존재하는데 1차캐시에는 없을 경우 조회(find)를 통해서 member2를 먼저 찾는다.
- JPA는 1차캐시에서 member2를 찾는데 없으므로 디비에 조회를 시도한다.
- 디비에는 member2가 있기 때문에 조회하고 member2를 1차캐시에 넣어준다.
영속 엔티티의 동일성 보장
- 1차 캐시가 있기 때문에 동일성이 보장된다.
- JPA에서 같은 트랜잭션안에서 실행하기 때문에 동일성이 보장된다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교 true
엔티티 등록 / 트랜잭션을 지원하는 쓰기 지연
EmtityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 본낸다.
transaction.commit(); // 트랜잭션 커밋
실제 내부적으로 JPA 동작 구조
- 영속 컨텍스트 안에는 1차캐시 공간뿐만 아니라 쓰기지연 SQL 저장소라는 공간이 존재한다.
- memberA를 저장하는 순간 내부적으로 memberA가 1차 캐시에 저장된다.
- 이와 동시에 JPA는 들어온 엔티티를 분석해서 쓰기 지연 SQL 저장소에 INSERT 쿼리를 만들어서 저장해놓는다.
- 마찬가지로 memberB가 들어왔을 경우 1차캐시에 memberB를 저장해놓는다.
- 이와 동시에 JPA는 쓰기 지연 SQL 저장소에 A때랑 마찬가지로 엔티티를 분석 후 쿼리를 만들어서 저장해서 차곡차곡 쌓아놓는다.
JPA 동작 구조 / transaction.commit();
- 트랜잭션을 커밋하는 시점에 쓰기 지연 SQL 저장소에 모아 두었던 쿼리들이 디비로 날라간다.
- JPA에서는 플러시(flush) 라고 표현한다.
- 모아두었다가 보낼 수 있기 때문에 버퍼링 기능을 사용할 수 있다. JDBC 배치 라고 얘기하고 Hibernate 에서는 옵션을 통해 설정할 수 있다.
<property name="hibernate.jdbc.batch_size" value="10"/>
<!-- value 사이즈 만큼 모아서 데이터베이스에 한번에 네트워크로 모아둔 쿼리를 보내고 커밋침 (버퍼링 기능)-->
엔티티 수정 - 변경 감지(Dirty Checking)
EntityManger em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
Member member = em.find(Member.class, 150L);
member.setName("DDDDDD");
// 수정된다고해서 수정하고나서 persist를 해줘야하지 않나 라고 생각하는데
// persist를 안하는것이 원칙이다 그냥 단순히 값만 바꿔주면 JPA가 트랜잭션이 커밋되는 시점에 알아서 변경해준다.
// em.persist(member);
System.out.println("===================");
tx.commit();
엔티티 수정 - 변경 감지(Dirty Checking) - 동작 원리
- 엔티티 값만 변경해줬는데 자동으로 update 문이 나가는데 이 원리는 영속성 컨텍스트 안에 존재한다.
- 트랜잭션을 커밋하는 순간에 내부적으로 flush()가 호출된다.
- JPA는 엔티티랑 스냅샷을 먼저 비교한다.
- 실제적으로 1차캐시라는 공간 안에는 Id와 Entity 그리고 스냅샷이라는 공간이 있다.
- JPA는 스냅샷과 비교해서 다를 경우에 UPDATE 쿼리문을 만들어서 쓰기 지연 SQL 저장소에 쌓아둔다.
- 그리고 JPA는 UPDATE 쿼리를 데이터베이스에 반영하고 나서 커밋하게 된다.
4. 플러시 (flush)
영속성 컨텍스트의 변경내용을 데이터베이스에 반영
플러시 발생
- 변경 감지
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시 하는 방법
- em.flush() - 직접 호출
- 트랜잭션 커밋 - 플러시 자동 호출
- JPQL 쿼리 실행 - 플러시 자동 호출
Member member = new Member(200L, "member200");
em.persist(member);
// 보통 persist하면 바로 디비로 날라가는것이 아니고 쌓아놓고 기다리는데
// 디비 날라가는거를 바로 확인하고 싶다 할 경우 플러시를 써준다.
// 플러시하는 방법
// 1. em.flush() - 직접 호출 방법
// 2. 트랜잭션 커밋 - 플러시 자동 호출
// 3. JPQL 쿼리 실행 - 플러시 자동 호출
// (JPA에서는 기본모드가 JPQL 쿼리 실행할 때는 무조건 플러시를 날려버린다. 그리고 쿼리가 날라감) / FlushModeType.AUTO
// 플러시는!!
// 영속성 컨텍스트를 비우는 것이 아닌 -> 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 것
// 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화 하면 됨
em.flush();
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을 쓰게되면 이대로라면 persist만으로는 디비로 쿼리가 날라가지 않았기 때문에
디비에 INSERT 자체가 안됬기 때문에 아무것도 조회해올 수 없는 상태가 됨
잘못하면 문제가 되는 상황이 있을 수 있기 때문에
JPA는 이 문제를 방지하기 위해 기본적으로 JPQL 쿼리를 실행할 때 flush()를 날려준다.
*/
플러시 모드 옵션
- 플러시 모드에도 옵션을 정해서 사용할 수 있다.
- 하지만 왠만해서는 건들지말고 default가 AUTO 이기 때문에 이상태로 그냥 사용하자!
플러시는!!
- 영속성 컨텍스트를 비우지 않음
- 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
- 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화 하면 됨
5. 준영속 상태
- 영속 -> 준영속
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
- 영속성 컨텍스트가 제공하는 기능을 사용 못함
준영속 상태로 만드는 방법
// 영속
Member member = em.find(Member.class, 150L);
/*
find를 했는데 아무런 결과값을 찾지 못하는 경우도 영속상태가 된다
JPA는 디비에 조회해서 아무것도 찾지 못했으므로
1차캐시에 넣기때문에 영속상태가 되는 것이다.
*/
member.setName("AAAAA");
// 준영속 상태가 됨
// 이렇게 하는 순간 JPA에서 관리를 하지 않겠다! 라는 거임
em.detach(member); //영속성 상태에서 특정 엔티티를 빼버리는것
em.clear(); // 영속성 상태를 통으로 초기화 해버리는 것
System.out.println("====================");
tx.commit();
자 여기까지 내용에 대해서 주저리 주저리 요약해서 설명을 적어놓았는데 .. 이해가 잘 안될 수 있잖아? 그치? 아닌가?
아무튼! 이해를 쉽게 돕기위해서 내가 이해한대로 비유를 통해 설명하고자 해
스타크래프트 2탄이 되겠군! 참 필자는 프로토스 유저여서 주된 유닛이 프로토스가 될꺼야
병력을 뽑아야 게임에서 이길 수 있잖아? 위 그림이 프로토스의 대표적인 유닛인 질럿과 드라군이야
우선 나는 얘네들을 마구마구 뽑을껀데.. 딱 요상태가 비영속 상태인거야
코드로 표현을 해보자면 ...
Member member = new Member(); // 질럿이나 드라군 등의 병력을 뽑기위해 객체를 만든다
Member.setId("member1");
member.setUsername("회원1");
그림으로 표현해보자면 ... 아래그림에서 딱 왼쪽의 상태이지 ...
자 이제 영속 상태를 설명해주기 위해 새로운 유닛을 소개해주려고 해
아비터라는 녀석이야 장기전으로 갔을 때 꼭 있어야하는 필수 유닛이지!
이녀석의 특징은 뭐냐면 ... 위에 떠있는 공중유닛인데 말이지..
유닛들 위에 올라가는 순간 캡처화면 처럼 흐리게 만들어주는 효과가 있어.. 적한테는 안보이게 되는거지!
이렇게 흐려지게 만드는 순간을 영속상태가 되었다고 표현할 수 있어..
정확히 말하자면 영속상태가 됨과 동시에 유닛들(질럿, 드라군) 은 1차캐시에 등록이되어 올라가는 상태인거지~!
코드로 표현을 해보자면...
//객체를 생성한 상태 = 유닛을 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// EntityManager = 아비터 소환!
EntityManager em = emf.createEntityManger();
em.getTransaction().begin();
// 객체를 저장한 상태 = 아비터가 유닛 머리위에 있는 상태(영속)
// member(질럿, 드라군)의 유닛은 1차캐시에 등록되어있음
em.persist(member);
그림으로 표현해보자면...
자 근데 여기서 문제가 하나 있어 ... 프로토스 유닛은 조합이 중요하거든? 근데 질럿 드라군만 가지고 뭘 하겠어
난 이녀석을 뽑아주려고 해...
JPA라는 녀석은 이녀석을 뽑아주기 위해서 1차캐시라는 공간에서 이녀석이 있는지 살펴봐
근데 말이지 ... 1차캐시에는 질럿이랑, 드라군이라는 녀석만 등록되어있고 이녀석은 없단말이야..
그래서 JPA라는 녀석은 데이터베이스인 게이트웨이한테 가서 조회를 해서 가지고 온다음에 1차캐시에다가 넣어줘
그러면 1차캐시에는 질럿, 드라군, 하이템플러 3개가 들어있게 되는거지..
그림으로 표현을 해보자면 ...
이제 병력들이 하나둘씩 모이게 되는데... 엄청 많아질꺼잖아?
스타크래프트는 이 병력들을 12마리당 한 부대로 지칭해서 부대컨트롤이 가능하게 되어있어...
드라군으로만 이루어진 1번 부대의 병력이 있어 ...
이 병력들은 이미 1차캐시에 등록이 되어있는 상태야 ...
JPA는 이 데이터 상태를 스냅샷으로 저장해서 1차캐시에 같이 올려놓게 되어있어
근데 병력구성이 바뀌었어 ... 다크템플러랑 질럿들이 섞여 있거든 .. 이거를 다시 1번부대로 지정하고 싶어
JPA는 이거를 감지하고 유닛(데이터)들을 스냅샷들과 비교를 해 ... 그리고 달라진 데이터가 있으면
쓰기 지연 SQL 저장소에다가 UPDATE 쿼리를 만들어 놓고 ... 디비에 반영을 하지
그림으로 표현을 해보자면 ...
병력 짱많이 모았다!!
자 이제 병력들도 어느정도 모았겠다 ... 적한테 쳐들어가야 하겠지?
지금 현재 많은 병력들이 영속 상태이면서 영속 컨텍스트 안에 1차캐시에 등록이 되어있는 상태이고
코드로는 이렇게 표현할 수 있어 ...
EmtityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
이 아비터라는 녀석은 리콜이라는 스킬을 사용할 수 있어 ... 지정한 공간에다가 리콜을 쓰면 병력들이 그진영으로 쓕 하고 소환되거든
트랜잭션 commit 하는 순간 이 리콜스킬을 플러시(flush)라고 표현할게..!
플러시는 트랜잭션을 커밋하는 순간에는 자동으로 실행되게 되고 다른 경우도 있어!
이렇게 플래시를 하게되면 쓰기지연 SQL 저장소에 쌓아두었던 쿼리들이 디비로 날라가게 되는거지! 실제 반영을 의미!
// 커밋하는 순간 데이터베이스에 INSERT SQL을 본낸다.
transaction.commit(); // 트랜잭션 커밋
// 플러시하는 방법
// 1. em.flush() - 직접 호출 방법
// 2. 트랜잭션 커밋 - 플러시 자동 호출
// 3. JPQL 쿼리 실행 - 플러시 자동 호출
결과적으로 플러시(flush)까지 이루어지면 이렇게 보여질 수 있는 거지!
'Dev > JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2020.07.28 |
---|---|
연관관계 매핑 기초 (0) | 2020.07.15 |
엔티티 매핑 (0) | 2020.07.12 |
JPA의 등장 (0) | 2020.06.30 |
SQL중심개발의 문제점 (0) | 2020.06.30 |