스프링 공부/JPA

[JPA] 3. JPA의 중요성 - ORM, Persistence Context

장아장 2022. 10. 19. 20:34

ORM을 위해 JPA를 쓴다는 것 까지는 이해했다. 하지만, 그 외에도 영속성 컨텍스트라는 중요한 개념이 존재한다. 

 

영속성 컨텍스트는 객체의 상태에 관한 것이다. 

객체는 데이터베이스에 저장되었는가, 아닌가로 구분된다기 보단, 생명주기와 관련되어 있다. 

 

영속성 컨텍스트 : 엔티티 자체를 영속화시킨다(DB에 저장된다는 것과는 조금 다른 개념이다)

 

객체가 영속화되었는가 아닌가를 기준으로 판단되며, 영속화된다는 것은 데이터베이스에 저장된다는 것과 동일한 개념은 아니다. 

 

엔티티의 생명 주기를 통해 영속성에 관해 이야기를 한다. 

비영속상태 영속성 컨텍스트와는 상관없는 새로운 상태의 객체
영속 상태 비영속상태의 객체를 영속화 시킨 상태
준영속 상태 영속성 컨텍스트에서 객체를 빼낸 상태
삭제 상태 객체 자체를 삭제시킨 상태

영속성 컨텍스트의 장점

 - 1차 캐시

  • 영속성 컨텍스트의 내부에 1차 캐시가 존재한다. (캐시값은 Id, Entity)
  • 저장한 이후에 조회를 할 때, 영속성 컨텍스트에서 먼저 1차 캐시를 찾는다. 
  • 있으면 캐시에 있는 값을 그냥 가져온다. 
  • if 캐시에 없다면 : 1차 캐시를 찾아보고, 없다면 DB에서 찾은 값을 1차 캐시에 저장시킨 후 반환한다. 
  •  근데, 애플리케이션 전체가 공유하는 캐시가 아니라, 트랜젝션 하나에서 작동되는 캐시

 - 동일성 보장

  • DB에는 repeatable read등급의 트랜잭션 격리 수준이 존재한다. 이는 데이터 하나를 기준으로 보았을 때, 설명해야 한다. 
  • 두개의 트랜잭션이 존재한다. 두 트랜잭션이 한 데이터를 읽어왔을 때, 각 트랜잭션에서 작업이 이루어진다. 
  • 1번 트랜잭션과 2번 트랜잭션이라고 할 때, 1번 트랜잭션이 데이터를 수정하고 커밋했다. 
  • 2번 트랜잭션에서 커밋하기 전에 데이터를 읽어오고, 다시 읽어왔을 때 그 데이터는 변한 값이 아닌, 원래 값을 나타낸다. 
  • 1차 캐시는 이러한 repeatable read등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션에서 제공해준다. 

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

  • JPA에서는 transaction.commit()을 하기 전까지 sql문을 데이터베이스로 보내지 않는다. 
  • 이는 commit()할 시에 sql문을 한번에 내보낸다. 
  • 새로운 객체를 생성하는 경우, 이 객체를 1차 캐시에 저장시키면서 sql저장소에 insert sql문을 만들어 저장해둔다. 
  • sql저장소에 모든 쿼리문이 저장되었다가, commit시에 일괄적으로 보내진다. 
  • 이를 통해 매 쿼리마다 트랜잭션을 만드는 것이 아닌, 트랜잭션의 조절이 가능하다. 

 - 변경 감지

  • 영속화된 객체의 값을 조회후 수정해 주면 자동으로 이를 영속화시킨다. 
  • 영속화되어있는 객체가 수정 되었을 시엔, commit때 1차 캐시에서 변경감지를 한다. 
  • 변경 감지 : ID(객체의 기본키), 객체, 스냅샷(처음 영속성 컨텍스트가 처음 언급되었을 때의 값)을 비교해, 변경이 있을 경우 이를 감지해준다. 
  • 커밋을 할 때 객체와 스냅샷을 비교하고, 변화가 있을 경우 쓰기지연 sql저장소에 update문을 만들어 저장시켜 DB에 적용시켜준다. 

 - 지연 로딩

  • Lazy, Eager을 통해 지연 로딩과 즉시 로딩으로 구분해 사용할 수 있다. 
  • Eager(즉시 로딩) : 외래키로 연관관계가 매핑된 객체를 불러올 때 외래키를 기본키로 가지는 객체도 바로 불러오게 한다. 이 때 추가로 가져온 객체는 프록시가 아닌 실제 객체이다. 
  • Lazy(지연 로딩) : 외래키로 표현시킨 객체는 프록시로 받아오고, 후에 필요할 때가 있다면 그 때 해당 객체를 다시 불러온다. 
  • 즉시로딩은 어떤 쿼리문으로 나갈지 예측할 수 없기 때문에, 지연 로딩을 통해 하나의 객체만 온전하게 불러오기 더 안전해진다. 

영속성 컨텍스트를 사용하는 것으로 이러한 장점을 가질 수 있다. 

또한, 위에서 몇번 언급되지만, 커밋을 할 때 변경을 감지하고, 쿼리문을 쓰기 지연 sql 저장소에 모았다가 한번에 보내는 등 데이터베이스로 가기 위한 과정들이 있는데, 이를 플러시(flush)라고 한다. 

 

플러시의 과정은 다음과 같다. 

  1. 변경을 감지한다.
  2. 수정 엔티티에 대한 update문을 쓰기 지연 SQL 저장소에 등록한다. 
  3. 지연 SQL 저장소의 쿼리문들을 데이터베이스에 전송시킨다. (여기에서 3번, 데이터베이스에 쿼리문들을 보내는 것은 또 3가지 방법이 있다. )
    1. 엔티티매니저.flush() <- 직접 호출하는 방법
    2. 트랜잭션.commit() <- 플러시 자동 호출 방법
    3. JPQL 쿼리 실행 <- 플러시 자동 호출

 

이러한 영속성 컨텍스트는 몇가지 특이점을 가진다. 

  • 영속성 컨텍스트는 엔티티 매니저와 1:1로 존재하며, 엔티티 매니저가 close될 때 까지 지워지지 않는다. 
  • 영속성 컨텍스트의 변경 내용을 DB에 동기화 시킨다. 
  • 커밋 직전에만 영속성 컨텍스트 동기화를 다 해주면 된다. 
  • 그러므로, 어디까지 동기화를 하고, 언제 커밋을 할지가 매우 중요하다.