영속성 컨텍스트란?
JPA를 이해하는데 가장 중요한 용어로 "엔티티를 영구 저장하는 환경"이라는 뜻이다.
영속성이라는 단어가 다소 생소할 수도 있지만 데이터가 소멸되지 않고 지속되는 상태라고 생각하면 된다.
EntityManagerFactory와 EntityManager
우선, 이를 이해하기 위해선 JAVA에서 제공하는 EntityManagerFactory와 EntityManager 클래스의 개념을 알아야 한다.
이전 포스팅에서 Entity에 대해 소개를 했는데, 이러한 Entity를 관리하는 역할을 수행하는 클래스가 EntityManager이다.
아래 그림과 같이 EntityManger는 내부에 영속성 컨텍스트(Persistence Context)를 두어 Entity들을 관리하게 되며,
JPA는 새로운 고객의 요청이 올 때마다 EntityManagerFactory를 통해 EntityManager를 생성하게 된다.
또한, Entity Manager는 내부적으로 데이터베이스 커넥션을 통해 DB를 사용하게 된다.
영속성 컨텍스트를 사용하는 이유
그러면 다시 돌아와서 EntityManager 내부에 영속성 컨텍스트를 둔다고 했는데, 어떤 역할을 하게 되는 것일까?
만약 EntityManger를 통해 memberA라는 객체를 DB에 저장한다고 해보자.
Member memberA = new Member();
memberA.setId("member1");
memberA.setUsername("회원1");
//1차 캐시에 저장됨. em은 EntityManagerFactory를 통해 생성된 EntityManager 객체이다.
em.persist(memberA);
EntityManager.persist(memberA)라는 코드를 실행하게 되면 내부적으로는 다음과 같이 동작한다.
memberA는 DB에 바로 저장되는 것이 아니라 영속성 컨텍스트를 통해 영속화가 된다. memberA는 영속성 컨텍스트 내에 1차 캐시에 저장이 되고 해당 동작에 대한 SQL 쿼리는 쓰기 지연 SQL 저장소에 저장이 된다.
추후 flush()가 호출이 되면 쓰기 지연 SQL 저장소에 있는 쿼리는 DB에 직접 날아가게 되는 메커니즘을 갖게 된다.
- 1차 캐시
이번엔 EntityManager를 통해 아래와 같이 DB에서 값을 조회한다고 가정해보자.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨. em은 EntityManagerFactory를 통해 생성된 EntityManager 객체이다.
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
member는 persist를 통해 1차 캐시에 저장이 되고 em.find를 통해 DB에서 값을 직접 가져오지 않고 1차 캐시에 저장된 값을 가져온다.
즉, 조회 시에 1차 캐시에서 조회를 한 후, 찾으려는 값이 1차 캐시에 없을 경우 DB에서 직접 조회를 하게 된다.
만약 아래와 같이 1차 캐시에 값이 없어 DB에서 직접 조회를 하게 될 경우 1차 캐시에 저장 후 반환을 해준다.
위와 같이 영속성 컨텍스트는 1차 캐시를 통해 성능 향상은 물론 매커니즘적으로 여러 장점을 갖는다.
- 동일성 보장
또한, 영속성 컨텍스트는 1차 캐시를 통한 동일성을 보장해주게 되는데, member1이 persist를 통해 1차 캐시에 저장된 상태에서 em.find를 통해 member1을 두 번 조회해도 1차 캐시에 있는 같은 레퍼런스의 객체를 조회하게 된다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b) // true, 1차 캐시를 통한 동일성 보장
- 쓰기 지연
위에서 잠깐 언급을 했는데, em.persist()를 하게 되면 멤버 객체 1차 캐시 저장, insert 쿼리를 쓰기 지연 SQL 저장소 저장을 하게 된다.
이렇게 쿼리들이 쓰기 지연 SQL 저장소에 쌓이게 되고 후에 flush()를 호출하게 되면 DB에 쌓인 쿼리를 보낸다.
flush()는 쿼리들을 DB에 보내서 DB와 싱크를 맞추게 해 주는데, 보통 커밋을 하게 되면 flush()는 자동으로 호출이 된다.
이렇게 사용하게 된다면 하나의 트랜잭션에서 여러 persist()를 호출하게 되었을 때 매 호출마다 DB에 쿼리를 날리지 않고 한 번에 모아서 날리기 때문에 성능적으로 더 좋다는 이점이 있다.
- 변경 감지(Dirty Checking)
쉽게 말해서 DB에 있는 데이터를 수정하게 될 때, update를 직접 해줄 필요가 없고 데이터 변경을 감지해서 알아서 쿼리를 실행해준다.
// 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("park");
memberA.setAge(27);
//em.update(member) 이와 같은 update 코드 필요 없음
transaction.commit(); // 커밋
만약 memberA를 find 후 해당 엔티티 객체의 username이나 age를 변경한다고 하자. 변경 후 따로 update 메서드와 같은걸 실행해줄 필요 없이 커밋을 하게 되면 내부적으로 update를 감지하고 이에 대한 쿼리를 날려주게 된다.
이에 대해 세부적인 동작원리를 보자면, flush() 시점에 Entity와 스냅샷을 비교해 변경된 Entity를 찾게 되고
변경된 Entity가 있으면 update 쿼리를 생성해 쓰기 지연 저장소에 저장, DB에 반영하는 메커니즘으로 동작하게 된다.
setXXX만 해줘도 변경을 감지해서 update 쿼리를 날려주니 정말 편하다고 볼 수 있다.
정리하자면 영속성 컨텍스트의 장점은
1. 1차 캐시
2. 동일성 보장
3. 쓰기 지연
4. 변경 감지(Dirty Checking)
이라고 볼 수 있다.
'JPA' 카테고리의 다른 글
엔티티(Entity)의 개념 (0) | 2022.02.17 |
---|---|
JPA 소개 (0) | 2022.02.11 |