RATSENO

[JPA]영속성 콘텍스트(persistence context) 본문

DEV/JPA

[JPA]영속성 콘텍스트(persistence context)

RATSENO 2020. 8. 3. 17:59

아직 많은 국내 프로젝트들은

스프링 프레임워크와 SQL Mapper인 마이바티스(MyBatis)를 엮어서 많이 진행되고 있습니다.

 

개발자들의 대부분의 업무는 반복적인 CRUD 쿼리 작성과, 쿼리와의 싸움으로 보내게됩니다.

 

하지만 요즘 변화의 바람이 많이 부는지 ORM진영의 JPA에 대한 공부의 필요성을 느끼게 되어 정리해 보려합니다.

차근차근 정리해 보겠습니다.

 


영속성 컨텍스트

JPA를 시작하게되면 가장 많이 듣는 단어가 엔티티, 영속성 컨텍스트 입니다. 그 만큼 JPA를 공부함에 있어서 가장 중요한 단어라는 뜻일것 같습니다. ORM, JPA는 엔티티라는 객체를 이용하는 방식입니다. 

이러한 엔티티"영구 저장하는 환경"이라는 어느 공간이라고 개념을 잡는것이 중요합니다. 

이 말은 영속성 컨텍스트는 눈에 보이지 않은 논리적인 개념이라는 뜻입니다. JavaScript를 공부하셨었다면 실행 컨텍스트를 들어 보셨을수도 있습니다. JS에서의 컨텍스트도 JPA에서의 컨텍스트와 비슷한 의미로 실행되는 영역이라고 볼수 있습니다. 

 

 

엔티티의 생명주기

회원가입하는 기능을 만들기 위해서는 회원이라는 엔티티를 만들어야합니다. 엔티티를 만든다는것은 소스상으로는 회원 클래스를 만든다는 것이며, 의미상(?)으로는 회원 객체를 만든다는 것이겠죠? 아래와 같이... 

public class Member{

    private Long id;
    
    private String username;
    
    //.....기타 등등
    //JPA관련 어노테이션들은 일단 생략했습니다.
    //참고로만 봐주세요
}

 

이러한 엔티티들은 JPA에서 생명주기라는 것을 가지고 있습니다. 간단하게 봐주세요

 

  • 비영속 (new/transient) - 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

  • 영속 (managed) - 영속성 컨텍스트에 의해 관리되는 상태

  • 준영속 (detached) - 영속성 컨텍스트에 저장되었다가 분리된 상태

  • 삭제 (removed) - 삭제된 상태

비영속

먼저 영속 컨텍스트는 Persistence에 의해서 만들어진 EntityManagerFactory를 통해  EntityManager를 생성하게되면EntityManager는 영속 컨텍스트와 맵핑이 됩니다. 위의 entityManager에 대한 설명입니다.

//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

영속성 컨텍스트에 의해 관리되지 않은 비영속 상태입니다.

 

영속

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//객체를 생성한 상태(비영속)
 
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
//"hello"-resources/META-INF 하위에 작성한 persistence.xml에서 작성한 persistenceUnitName

EntityManager em = emf.createEntityManager();
//영속성 컨텍스트

EntityTransaction tx = em.getTransaction();
tx.begin();
//트랜잭션 시작

em.persist(member);
//객체를 저장한 상태(영속)

위와같이 비영속상태인 엔티티를 EntityManager의 persist() 메서드를 사용하여 member 객체를 저장하는 순간

Member 엔티티는 영속성 컨텍스트의 관리를 받는 영속 상태가 됩니다.우리가 회사에 들어가게 되는 순간 회사의 관리를 받게되는 것과 같은것입니다...ㅠ여기에서 저장이라는 의미는 DB에 쿼리가 날라가서 DB에 저장되는것이 아닙니다.영속성 컨텍스트라는 공간에 저장 관리되는 것입니다.

 

준영속

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

 

삭제 - DB에서 삭제

//객체를 삭제한 상태(삭제)
em.remove(member);

 

마이바티스를 사용하였을때는 예를들어 회원을 저장하는 쿼리를 호출하게 되면, 바로 DB에 저장이 되었습니다.

하지만 지금까지의 JPA 내용을 보게되면 DB에 바로 맞닿아 있는 것이 아닌,

중간에 하나의 계층(영속성 컨텍스트)이 있는것으로 느껴집니다.

 

이러한 방식은 여러가지 이점을 가져옵니다.

  • 1차 캐시

  • 동일성(identity) 보장

  • 트랜잭션을 지원하는 쓰기 지연

  • 변경 감지 (Dirty Checking)

  • 지연 로딩 (Lazy Loading)

 

영속성 컨텍스트의 이점

@1차 캐시(엔티티 조회)

//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//엔티티를 영속
entityManager.persist(member);

영속성 컨텍스트 = entityManager = 1차 캐시 이렇게 느슨하게 서로 같다고 생각해보도록 하겠습니다.

persist(member)를 통하여 member객체는 영속성 컨텍스트 내부의 1차 캐시에 저장됩니다.

 

그림에서 @Id는 member객체의 id 속성, 즉 DB의 회원 테이블의 PK값입니다.

Entity는 member객체를 생성하여 1차 캐시에 저장한 Member 엔티티 자체입니다. 

Map을 생각하시면 이해가 수월하실 겁니다.

 

현재의 상태는 persist() 메소드를 통하여 member 객체가 1차 캐시에 저장된 상태입니다.

이 상태에서 "member1"을 Id로 가진 회원을 조회하겠습니다.

//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//엔티티를 영속
//1차 캐시에 저장됨
entityManager.persist(member);

//"member1"이라는 Id를 가진 엔티티 조회
//1차 캐시에 있기때문에 1차 캐시에서 바로 조회
Member findMember = em.find(Member.class, "member1");

find라는 조회 메서드를 사용하여 Member엔티티를 조회하게되면, DB에서 조회를 할것으로 예상할수 있지만

JPA는 영속성 컨텍스트의 1차 캐시에서 먼저 조회를 합니다. 

이때 1차 캐시에 해당 엔티티가 존재할 경우, 1차 캐시에 캐시되어있는 값을 조회를 하여 반환합니다.

 

다음으로 "member2"라는 Id를 가진 회원이 1차 캐시에는 없고 DB에 이미 저장되어있다고 가정하였을때를 보겠습니다.

//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//엔티티를 영속
//1차 캐시에 저장됨
entityManager.persist(member);

//"member1"이라는 Id를 가진 엔티티 조회
//1차 캐시에 있기때문에 1차 캐시에서 바로 조회
Member findMember = em.find(Member.class, "member1");

//"member2"이라는 Id를 가진 엔티티 조회
//1차 캐시에는 없기 때문에 DB에서 조회
//DB에서 조회된 값, 1차 캐시에 저장
//반환
Member findMember2 = em.find(Member.class, "member2");

"member2" 를 Id로 가진 엔티티가 1차 캐시에 없기 때문에, DB에서 조회를 하게되고

1차 캐시에 저장되어 반환됩니다.

 

 

@영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.cass, "member1");

System.out.println(a==b);
//동일성 비교 true

같은 엔티티를 조회할 경우, 영속성 컨텍스트는 엔티티에 대한 동일성을 보장합니다.

1차 캐시로 반복 가능한 읽기,조회(REPEATABLE READ)등급의 트랜잭션 격리 수준을

데이터베이스가 아닌 애플리케이션 차원에서 제공합니다. 자바 컬렉션에서 같은 객채를 꺼낼때의 수준과 같은 것입니다.

 

@엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야한다.

transaction.begin(); // 트랜잭션 시작

em.persist(memberA);
em.persist(memberB);
//여기까지는 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다
transaction.commit();// 트랜잭션 커밋

persist()메서드를 통해 memeberA, memberB 객체를 저장하게되면, 이때 DB에 INSERT SQL이 날아가는것이 아닙니다.

transaction.commit()을 하게되는 수는 데이터 베이스에 INSERT SQL을 보내게됩니다.

persist()를 호출하게 되면, 먼저 저장하려는 엔티티(memberA, memberB)는 1차 캐시에 저장됩니다.

그리고 JPA는 엔티티를 분석하여 INSERT SQL을 생성하고, 영속성 컨텍스트에 존재하는 쓰기 지연 SQL 저장소에

해당 INSERT SQL을 저장합니다. 이때까지도 INSERT SQL은 DB에 날라가지 않는 상태입니다.

 

실제 데이터베이스에 INSERT SQL이 날라가게 되는 시점은, transaction.commit()한 후에 쓰기 지연 SQL 저장소에 있던 INSERT SQL이 flush()되면서[추후에 설명될 내용입니다.]  데이터베이스 커밋이 발생하면서 DB에 저장됩니다.

해당 기능은 JPA설정 파일의 옵션을 통하여 적용할수 있습니다. 

 

@엔티티 수정 -  변경 감지 (Dirty checking)

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

//영속 엔티티 조회
//조회된 memberA의 username 은 "hello"
Member memberA = em.find(Member.class, "memberA");

//영속 엔티티 수정
memberA.setUsername("hi");

//업데이트 코드가 필요할 것 같지만, 아니다.
//em.update(memberA);

//이 코드 또한 필요 없다.
//em.persist(memberA);

tx.commit();

MyBatis를 사용하였을 때는 조회한 객체를 수정하고, 다시 업데이트하는 비지니스 로직이 필요했습니다.

하지만 위의 코드처럼 JPA에서는 조회한 memberA에 대해 수정하고 업데이트하는 부분이 주석처리 되어있습니다.

tx.commit()만으로 DB에서는 해당 값이 수정이 되어있습니다.

최초 조회를 하게되면 영속성 컨텍스트의 1차 캐시에 엔티티들이 저장이 되는데,

이때 그림과 같이 엔티티의 스냅샷을 같이 가지게 됩니다.

tx.commit()을 통해 flush()가 호출되면 수정된 엔티티와 1차 캐시의 엔티티 스냅샷을 비교하여 변경된 부분을 감지하고

JPA는 자동으로 UPDATE SQL을 생성, DB에 커밋이 되면서 반영이됩니다.

개발자가 업데이트 로직을 구성할 필요 없이 JPA에서 자동으로 처리해 주는 것이죠.

 

@엔티티 삭제

//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

em.remove(memberA); //엔티티 삭제

 

영속성 컨텍스트에 대해서 개념적인 내용을 간단하게 정리해 보았습니다.

잘못된 부분이 있다면 편하게 지적해주세요!

감사합니다.

 

 

참고 및 출처

도서 : 자바 ORM 표준 JPA 프로그래밍  - 저자 : 김영한 님

강의 : 자바 ORM 표준 JPA 프로그래밍 (기본편) - 강사 : 김영한 님

'DEV > JPA' 카테고리의 다른 글

[JPA]플러시 (flush)  (0) 2020.08.04
Comments