영속성 컨텍스트

D A S H B O A R D
D E V E L O P
S E C U R I T Y
 영속성 컨텍스트란?
 Entity Manager와 Persistence Context
 엔티티 생명주기
 영속성 컨텍스트의 이점
 플러시
Reference

 영속성 컨텍스트란?

JPA에서 영속성 컨텍스트(Persistence Context)란 객체(Entity)를 영구 저장하는 환경으로 볼 수 있는 논리적 개념이다.(실존하지 않는다!)
이 환경에서 객체(Entitiy) 정보가 저장되고, 엔티티 매니저(Entity Manager)를 통해 영속성 컨텍스트에 접근하게 됩니다.
이를 통해 엔티티의 상태 변경을 감지하고, 변경 사항을 데이터베이스에 반영하게 됩니다.
영속성 컨텍스트를 간단히 생각하면 1차 캐시와 같다, Entitiy Manager와 같다고 생각해도 된다.(하지만 약간의 차이가 있으니 완전히 같다고 생각하진 말자!)
영속성컨텍스트1차캐시EntityManager영속성 컨텍스트 \fallingdotseq 1차 캐시 \fallingdotseq Entity Manager

 Entity Manager와 Persistence Context의 관계

J2SE

⇒ 엔티티 메니저와 영속성 컨텍스트가 1:1

J2EE

⇒ 엔티티 매니저와 컨텍스트가 N:1 매핑
J2SE vs J2EE
Java SE(Standard Edition)
자바 스탠다드 에디션은 가장 보편적으로 쓰이는 자바 API집합체다. 예전에는 J2SE로 불렸으나 버전 6.0이후에 Java SE로 변경되었다. 이전에는 썬마이크로시스템즈에서 관리했으나 지금은 JCP주도하에 개발되고 있다. 일반 자바 프로그램 개발을 위한 용도로 사용되며 스윙이나 AWT와 같은 GUI방식의 기본 기능이 포함된다.
Java EE(Enterprise Edition) == jakarta EE
자바 엔터프라이즈 에디션은 자바를 이용한 서버측 개발을 위한 플랫폼이다. Java EE는 표준 플랫폼인 Java SE를 사용하는 서버를 위한 플랫폼이다. 전사적 차원(대규모의 동시 접속과 유지가 가능한 다양한 시스템의 연동 네트워크 기반 총칭)에서 필요로 하는 도구로 EJB, JSP, Servlet, JNDI 같은 기능을 지원하며 WAS(Web Application Server)를 이용한 프로그램 개발 시 사용된다.

 엔티티 생명주기

비영속(New/transient) ⇒ 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
영속(Managed) ⇒ 영속성 컨텍스트에 관리되는 상태
준영속(Detach) ⇒ 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(Remove) ⇒ 삭제된 상태

비영속

//객체를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setUsername("회원1");
Java
복사
⇒ 객체를 처음 생성한 상태가 비영속이라 보면 됨

영속

//객체를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setUsername(“회원1); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(member); //객체를 저장한 상태(영속)
Java
복사
⇒ Entity Manager를 통해 persist 함수를 호출하면 영속한 상태라 보면 됨 (영속할 수 있는 조건은 많다! → 뒤 1차캐시 참고)

준영속, 삭제

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태 em.detach(member); // 영속성 컨텍스트를 모두 초기화 (안에 있는 엔티티를 모두 준영속으로 만든다.) em.clear(); // 영속성 컨텍스트를 종료 em.close();
Java
복사
⇒ 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
⇒ 영속성 컨텍스트에 존재하는 객체를 Entity Manager로 하여금 관리하지 않도록 분리
//객체를 삭제한 상태(삭제) em.remove(member);
Java
복사
⇒ 영속성 컨텍스트에 존재하는 엔티티(객체)를 삭제

 영속성 컨텍스트의 이점

지연 로딩 (Lazy Loading) ⇒ 현업에서 사용하는 아주 중요한 개념 (마지막에 간단히 설명 자세한 내용은 다음에)

엔티티 조회 - 1차 캐시

1차 캐시에 저장된다면 해당 객체는 영속한다고 본다. ⇒ 엔티티 매니저가 관리하기 때문

1. 1차 캐시

//엔티티를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setUsername("회원1"); //엔티티를 영속 em.persist(member);
Java
복사
엔티티를 영속 상태로 두게되면, 영속성 컨텍스트에 1차 캐시에 해당 엔티티가 저장된다.
persist()를 진행했다고 바로 DB에 commit되지 않는다. ⇒ 트랜잭션을 지원하는 쓰기 지연

2. 1차 캐시에서 조회

Member member = new Member(); member.setId("member1"); member.setUsername("회원1"); //1차 캐시에 저장됨 em.persist(member); //1차 캐시에서 조회 Member findMember = em.find(Member.class, "member1");
Java
복사
Entity Manager로 엔티티 조회 시 바로 DB에 쿼리를 날려 새로운 정보를 얻어 오는 것이 아닌 1차로 1차캐시를 먼저 확인한 후 1차 캐시에 있다면 해당 정보를 가져온다.

3. 데이터베이스에서 조회

Member findMember2 = em.find(Member.class, "member2");
Java
복사
Entity Manager로 엔티티 조회할 시에 1차로 1차 캐시에 저장된 정보를 확인 후 없다면 DB에 쿼리를 날려 DB로 부터 정보를 가지고 온 후, 1차 캐시에 저장하고 해당 엔티티를 반환해준다.
이렇게 가져온 엔티티도 1차 캐시에 저장되기 때문에 영속하게 된다.

동일성 보장 - 동일성(identity) 보장

Member a = em.find(Member.class, "member1"); Member b = em.find(Member.class, "member1"); System.out.println(a == b); //동일성 비교 true
Java
복사
같은 객체를 2번 조회하지만, 같은 객체를 가지고 오는 것을 알 수 잇다.
위 1차 캐시에서 말한것과 같이 처음 Member a를 조회할 때는 1차 캐시에 없기 때문에, DB에 쿼리를 날려 엔티티를 가지고 온다.
Member b를 조회할 경우에는 1차 캐시에 해당 엔티티가 존재하기 때문에 따로 DB에 쿼리를 날리지 않고 가지고 오기 때문에, 같은 엔티티가 반환되는 모습을 볼 수 있다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭 션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다고 표현

엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)

EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다. transaction.begin(); // [트랜잭션] 시작 em.persist(memberA); em.persist(memberB); //여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. transaction.commit(); // [트랜잭션] 커밋
Java
복사

1. em.persist(memberA)

2. em.persist(memberB)

3. tansaction.commit()

persist()시에는 1차 캐시에 Entity가 저장되고 Query는 쓰기 지연 저장소에 저장된다. → 바로 DB로 Query가 날라가지 않는다는 것이다.
transcation을 commit할 경우 이 때 DB에 flush()를 통해 SQL 쿼리를 보내게 되며 저장된다. → flush()는 뒤 참고
이렇게 transaction commit 기능을 통해 SQL 구문 날리는 것을 뒤에서 한거번에 하기 때문에, DB와의 연결을 줄일 수 있고 중간에 값이 변해도 관리하기 쉽다.
아래 코드를 이용하면 영속상태의 엔티티가 batch size만큼 생기면 commit 전에 쿼리를 보내도록 설정해 줄 수 잇다.
<property name="hibernate.jdbc.batch_size" value="10"/>
XML
복사

엔티티 수정 - 변경 감지

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(); // [트랜잭션] 커밋
Java
복사
해당 코드를 보면 persist()라든지, update()라든지 변경 사항을 저장하는 코드가 존재하지 않는다.
영속성 컨텍스틑에서 관리하는 엔티티의 경우 transaction.commit() 시에 변경 사항을 체크 후 변경되었다면 Update 쿼리를 만들어서 보내주기 때문에 따로 변경사항을 저장하는 코드가 필요하지 않다.

지연 로딩

Team team = new Team(); team.setName("teamA"); em.persist(team); Member member = new Member(); member.setUsername("member1"); member.setTeam(team); em.persist(member); em.flush(); em.clear(); Member findMember = em.find(Member.class, member.getId());
Java
복사

즉시 로딩일 경우(EAGER)

Hibernate: select member0_.MEMBER_ID as MEMBER_I1_0_0_, member0_.TEAM_ID as TEAM_ID3_0_0_, member0_.USERNAME as USERNAME2_0_0_, team1_.TEAM_ID as TEAM_ID1_1_1_, team1_.name as name2_1_1_ from Member member0_ left outer join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=?
Plain Text
복사
→ 분명 Member를 조회했는데 왜 Team까지 join되어 쿼리가 나간다.
정리 ⇒ 결론은 현업에서는 LAZY로 설정하여 사용한다!!!
비지니스 로직 상 Member 데이터가 필요한 곳에 대부분 Team의 데이터 역시 같이 사용 할 필요가 있다면 어떨까? FetchType을 EAGER로 설정하여 항상 Member와 Team을 같이 조회해오는 것이 더 좋을 것이다.
Member를 사용하는 곳 대부분에서 Team 데이터가 필요하지 않다면? FetchType을 LAZY로 설정하여 Member만 조회하고, 드물게 Team이 필요할 땐 그 때 Team에 대한 쿼리를 한번 더 날려 조회하는것이 좋을 것이다.

지연 로딩일 경우(LAZY)

Hibernate: select member0_.MEMBER_ID as MEMBER_I1_0_0_, member0_.TEAM_ID as TEAM_ID3_0_0_, member0_.USERNAME as USERNAME2_0_0_ from Member member0_ where member0_.MEMBER_ID=?
Plain Text
복사
→ 이번엔 Member만 딱 조회해오는것을 볼 수 있다.
→ 하지만 아래 코드를 실행 했을 때 Team을 조회하는 쿼리가 나간다.
findMember.getTeam().getName(); // 코드 자체엔 큰 의미가 없음, team 객체의 데이터를 요구하기 위한 코드
Java
복사
Hibernate: select team0_.TEAM_ID as TEAM_ID1_1_0_, team0_.name as name2_1_0_ from Team team0_ where team0_.TEAM_ID=?
Plain Text
복사

 플러시

영속성 컨텍스트의 변경내용을데이터베이스에 반영해준다.

변경 감지
수정된 엔티티 쓰기 지연 SQL 저장소에 등록
쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
플러시는!
영속성 컨텍스트를 비우지 않음
영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화 하면 됨

영속성 컨텍스트를 플러시하는 방법

em.flush() - 직접 호출
트랜잭션 커밋 - 자동 호출
JPQL 쿼리 실행 - 자동 호출
플러시 옵션 - em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.AUTO ⇒ 웬만하면 변경하지 말고 기본값 사용!!!! → 커밋이나 쿼리를 실행 할 때 플러시(기본 값)
FlushModeType.COMMIT → 커밋할 때만 플러시

em.flush()

직접 호출하는 일은 거의 없지만, Test Code 작성 시에 필요한 경우가 있으니 알아두자!

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();
Java
복사
만약 DB가 비어 있다고 가정하자
이 때 persist로 memberA, B, C를 영속상태로 두었지만 아직 Commit되지 않은 상태이기 때문에 여전히 DB는 비어 있다.
여기서 JPQL을 실행할 경우 DB는 비어 있기 때문에 가져올 수 있는 엔티티 자체가 없게 된다.
⇒ 이러한 경우가 발생하면 나중에 문제가 생길 수 있다. 그렇기 때문에 JPA는 JPQL 작성 시 바로 그냥 flush를 날린다.