본문 바로가기
IT 개인 공부/JPA

[JPA] 영속성 컨텍스트

by Libi 2021. 8. 31.
반응형

영속성 컨텍스트란 엔티티를 영구 저장하는 환경이란 의미이다. JPA는 실제로 내부에서 영속성 컨텍스트를 기반으로 동작한다.

즉, 엔티티 매니저를 통해 엔티티를 Persist할 경우 DB에 저장되는 것이 아니라 영속성 컨텍스트라는 논리적인 저장소에 저장된다.

영속성 컨텍스트를 공부하기 전에 엔티티 매니저 팩토리와 엔티티 매니저에 대해서 간단하게 알고 넘어가자.

 

JPA는 클라이언트의 요청을 엔티티 매니저를 통해 처리하고 내부적으로 DB Connction Pool을 사용해서 DB에 접근한다.

엔티티 매니저 팩토리는 바로 엔티티 매니저를 생성해주는 친구이다. 

웹 애플리케이션에서는 엔티티 매니저 팩토리를 하나 생성하여 클라이언트의 요청이 올 때마다 새로운 엔티티 매니저를 생성하여 제공한다.

이는 스프링같은 프레임워크에서 제공해주는 서비스이다.

 

  • 엔티티 매니저 팩토리는 DB당 하나만 생성해야 한다.
  • 엔티티 매니저는 스레드가 하나 생성될 때마다 생성되는데 스레드 간에 공유해서는 안된다.
  • 더 이상 사용하지 않을 경우 반드시 자원을 반환해줘야 한다.
  • 데이터를 변경하는 모든 작업은 반드시 Transaction 안에서 이루어져야 한다.

 

그렇다면 본격적으로 영속성 컨텍스트에 대해서 알아보도록 하자.

저장, 수정 등의 요청이 발생하면 엔티티 매니저가 이를 처리한다고 하였다. 이때 내부에서 영속성 컨텍스트에 저장하고 관리한다.

엔티티 매니저와 영속성 컨텍스트는 1:1 관계이다. 따라서 영속성 컨텍스트는 하나의 트랜잭션을 의미한다.

 

따라서 반드시 트랜잭션 Begin과 Commit 시점 사이에서 데이터를 조작해야 한다. 트랜잭션 범위를 벗어난 작업은 영속성 컨텍스트 관리 대상이 아니기 때문에 실제 DB에 반영되지 않는다.

즉, 트랜잭션 Commit 시점에 DB에 쿼리가 날아가고 작업들이 반영된다.

 

그렇다면 왜 DB를 바로 접근하지 않고 영속성 컨텍스트라는 곳을 한 번 거쳐서 DB를 접근하는 것일까?

이는 패러다임의 불일치 문제를 해결하거나 성능상의 이점들을 얻을 수 있기 때문이다.

  • 1차 캐시
  • 영속 엔티티의 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

 

하나씩 알아보기 전에 엔티티의 생명주기에 대해서 먼저 알아보자.

엔티티 생명주기는 엔티티가 영속성 컨텍스트와의 관계에 따라 4가지의 상태가 존재한다.

  • 비영속(New/Transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태, 영속성 컨텍스트에서 관리하지 않기 때문에 어떠한 작업을 수행해도 DB에 반영되지 않는다.
  • 영속(Managed) : 영속성 컨텍스트에 관리되는 상태, 영속성 컨텍스트에서 관리되기 때문에 트랜잭션 Commit 시점에 관리되는 모든 엔티티들이 DB에 반영된다.
  • 준영속(Detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태로 영속성 컨텍스트에서 엔티티를 지운 상태, 비영속 상태와 다른 점은 영속성에서 관리된 적이 있기 때문에 PK를 가지고 있다는 점이다.
  • 삭제(Removed) : DB에게 삭제를 요청한 상태

 

EntityManagerFactory emf = Persistence.createEntityManagerFactory("Jpa"); //엔티티 매니저 팩토리 생성
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction tx = em.getTransaction(); //트랜잭션 생성
tx.begin(); //트랜잭션 시작

try {
    //비영속 상태
    Member member = new Member();
    member.setUsername("Libi");
    //영속 상태 : 영속성 컨텍스트에 저장, DB(x)
    em.persist(member);
    //준영속 상태 : 영속성 컨텍스트에 관리되기 위해선 PK 값이 필수
    //엔티티 PK 전략에 따라 PK를 자동으로 설정 
    em.detach(member);
    //삭제 상태
    em.remove(member);
    tx.commit(); //실제 쿼리들이 DB에 반영
} catch (Exception e) {
    tx.rollback(); //문제 발생시 작업들을 취소하고 정상이었던 이전 상태로 롤백
} finally {
    em.close(); //엔티티 매니저는 connection을 하나 물기 때문에 사용이 끝났으면 반드시 자원 반환
}

emf.close(); //애플리케이션 종료시 반드시 자원 반환

 

그렇다면 영속성 컨텍스트를 사용함으로써 가져오는 이점들에 대해서 알아보자.

 

 

1차 캐시

 

영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 말 그대로 캐시(Cache)를 생각하면 된다.

 

영속성 컨텍스트는 하나의 트랜잭션이라고 하였다.

즉, 트랜잭션 범위에서 자주 사용되는 엔티티를 조회할 때 매번 DB를 통해 조회하지 않고 조회한 엔티티를 1차 캐시에 저장함으로써 다음에 빠르게 조회할 수 있는 이점이 있다.

하지만 사실상 한 트랜잭션 범위에서 사용되는 캐시이기 때문에 트랜잭션 범위가 엄청나게 넓지 않은 이상 크게 성능에 이점이 있지는 않다.

 

 

영속 엔티티의 동일성 보장

 

영속성 컨텍스트에서 관리하는 엔티티는 동일성을 보장해준다.

Member memberA = em.find(Member.class, 1L);
Member memberB = em.find(Member.class, 1L);
System.out.println(memberA == memberB); //true

 

즉, Java의 Collections에서 객체를 조회하는 것과 동일하게 사용할 수 있다.

이는 1차 캐시를 통해 제공하는 기능이다.

1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공해준다.

 

 

트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)

 

영속성 컨텍스트는 트랜잭션 Commit이 발생하는 시점에 쿼리를 DB에 보낸다고 하였다.

100개의 엔티티를 insert 했을 경우 1개의 엔티티마다 insert 쿼리를 DB에 날리는 것보다 100개의 insert 쿼리를 모은 다음에 쿼리를 DB에 날리는 것이 DB와 Connection 하는 비용이 적게 들기 때문에 효율적이다.

영속성 컨텍스트는 쓰기 지연 SQL 저장소라는 곳에 쿼리들을 모아놨다가 트랜잭션 Commit이 발생하는 시점에 저장된 쿼리를 DB로 날린다.

 

실제로는 flush가 발생하면 쿼리가 날아간다. 트랜잭션 Commit은 내부적으로 flush 기능을 포함한다.

주의해야 할 점은 flush는 영속성 컨텍스트를 비우는 것이 아니다. 단지 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 작업이다.

또한, 쓰기 지연 SQL 저장소에 모으는 쿼리의 개수는 batch_size로 조절할 수 있다.

이러한 최적화를 잘 활용하면 성능을 굉장히 향상시킬 수 있다.

 

 

엔티티 수정(변경 감지 : Dirty Checking)

 

객체의 값을 변경하는 코드만 작성하여도 영속성 컨텍스트에서 알아서 Update 쿼리를 만들어서 DB에 반영해준다.

//영속 엔티티 조회
Member member = em.find(Member.class, 1L);
//영속 엔티티 수정
//em.update(member)같은 코드가 없이 객체 값만 수정하여도 알아서 Update 쿼리를 만들어줌
memberA.setUsername("Hello");
transaction.commit();

 

이 역시 1차 캐시를 활용하는 기능이다.

1차 캐시는 스냅샷이라는 필드를 가지고 있는데 이는 1차 캐시에 저장할 때의 엔티티 상태를 의미한다.

 

flush가 호출되면 현재의 엔티티와 1차 캐시의 스냅샷과 비교해서 다르다면 Update 쿼리를 쓰기 지연 SQL 저장소에 만들어서 DB에 반영한다.

이러한 메커니즘 덕분에 우리는 엔티티의 값만 변경하는 코드만 작성하며 된다.

삭제도 동일한 메커니즘으로 동작하며 Delete 쿼리가 생성된다.

 

 

지연 로딩(Lazy Loading)

 

Member와 Team 엔티티가 있고, Member는 하나의 Team 가진다고 하자.

class Member {
    Long id;
    String name;
    Team team;
}

class Team {
    Long id;
    String name;
}

 

Member를 조회해서 사용한다고 하면, 로직에 따라서 Member와 Team 모두 사용할 수도 있고 Member만 사용할 수도 있을 것이다.

전자인 경우는 당연히 Member를 조회할 때 Team도 같이 Join 연산을 통해 가져오는 것이 좋지만 후자인 경우는 사용하지도 않는 Team 객체를 위해 Join 연산을 추가적으로 실행하기 때문에 비효율적이다.

이는 지연 로딩을 통해 해결할 수 있다. 지연 로딩을 해당 엔티티를 실제로 사용하는 시점에 쿼리를 날려서 값을 가져오는 방식이다.

Member member = em.find(Member.class, 1L); //Member와 Team은 지연 로딩 관계
System.out.println(member.getName()); //아직 team은 비어있음
System.out.println(member.getTeam().getName()); //team을 실제로 사용하는 시점에 쿼리를 날려 값을 저장

 

 

 

[ Reference ]

· https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

반응형

'IT 개인 공부 > JPA' 카테고리의 다른 글

JDBC vs SQL Mapper vs ORM  (0) 2021.08.29

댓글