BackEnd/Spring

JPA 영속성 관리 - 내부 동작 방식

PgmJUN 2023. 3. 20. 00:04

✔ JPA에서 가장 중요한 2가지

1. 객체와 관계형 데이터베이스 매핑하기
DB와 객체를 어떻게 설계해서 어떻게 매핑되도록 할 것인가(설계와 관련된 부분)
 
2. JPA의 동작원리
영속성 컨텍스트
 


⚙ 엔티티 매니저 팩토리와 엔티티 매니저

  • 클라이언트의 요청이 올때마다 EntityManagerFactory에서 EntityManager를 생성해서 제공한다.
  • EntityManager는 내부적으로 DB Connection을 사용해서 DB를 사용하여 데이터를 제공함.

 

📦 영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어
  • 직역하자면 “엔티티를 영구 저장하는 환경”이라는 뜻
  • EntityManger.persist(entity) 시에 DB에 저장한다는 뜻이 아닌 영속성 컨텍스트를 통해 엔티티를 영속화 한다는 뜻
  • → DB저장이 아닌, 영속성 컨텍스트에 저장*

 

영속성 컨텍스트는 어떻게 확인해? 🤷🏻‍♂️

  • 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.
  • 엔티티 매니저를 통해 접근이 가능하다.

 


🔀 환경에 따른 차이

 

J2SE 환경에선

 
 

엔티티 매니저와 영속성 컨텍스트가 1:1로 연결
 

스프링 프레임워크 같은 컨테이너 환경에선

엔티티 매니저와 영속성 컨텍스트가 N:1로 연결
 


엔티티의 생명주기

그럼 영속성 컨텍스트에 저장되는 엔티티의 생명주기는 어떻게 되는걸까?
바로 다음과 같다.
 

  • 비영속(new/transient)
    영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속 (managed)
    영속성 컨텍스트에 관리되는 상태
  • 준영속 (detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed)
    삭제된 상태

 


1. 비영속(new/transient)

JPA에 상관 없이 객체만 생성한 상태로 영속성 컨텍스트와는 아무 관련이 없는 상태이다.
 


2. 영속(managed)

비영속 상태인 객체를 EntityManger를 통해 persist해주면 영속 상태가 되는데,
이는 영속성 컨텍스트에 엔티티가 올라갔음을 뜻한다.
 
이는 직전에 말했듯이 DB에 저장되는 것이 아닌 영속성 컨텍스트에 올리는 것이기 때문에,
Hibernate에서 Insert 쿼리가 실행되어 실제로 DB에 데이터가 저장되는 시점은
persist를 입력한 시점이 아닌 EntityManger의 Transaction이 commit되었을 때이다.
 

그렇기에 아래와 같은 코드를 실행 시,

 
쿼리는 BEFORE AFTER 문자열 사이가 아닌
우리가 예상한 대로 commit시점에서 발생되는 것을 시각적으로 확인할 수 있다.

 


 

3. 준영속(detached)

영속성 컨텍스트에서 엔티티가 분리된 상태를 뜻한다.
 
EntityManger의 detach 메서드를 통해,
영속성 컨텍스트에서 엔티티를 분리할 수 있다.

 
그렇기 때문에 아래와 같은 코드를 실행 시,

 
트랜젝션을 커밋해도 아무런 쿼리문이 나타나지 않는다.

 


 

4. 삭제(removed)

영속성 컨텍스트에서 엔티티를 완전히 지우는 것을 뜻한다.

아래와 같은 코드 실행 시,

detach 했을 때와 같이 아무런 일도 벌어지지 않는다.
 


✨ 영속성 컨텍스트의 이점

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
  • 변경 감지(Dirty Checking
  • 지연 로딩(Lazy Loading)

 
 

1. 엔티티 조회, 1차 캐시

EntityManager를 통해 Entity를 persist 했을 때
엔티티가 영속성 컨텍스트에 저장이 되는데,

이때 그냥 저장되는 것이 아닌 ‘1차 캐시’ 라는 것에 저장이 된다.
 
 
1차 캐시는 다음과 같은 Key-Value 형태의 구조를 가진다.

Key : DB의 기본키
Value : 엔티티

 
그렇다면 이와 같은 1차 캐시는 어떻게 사용될까?
 
 

1차 캐시에서 조회

EntityManger를 통해 find 명령으로 DB를 조회했을 때,
DB를 먼저 찾는 것이 아니라 영속성 컨텍스트의 1차 캐시를 먼저 조회한다.
 
그리고 find 시에 입력받은 id값이 1차 캐시에 존재하면 그 값을 찾아 리턴해준다.
 

그럼 위와 같이 persist를 한 다음, find를 수행하는 코드가 있다면,
find 수행 시 DB를 조회하지 않고 1차 캐시의 값을 가져올 것이기 때문에
select 쿼리는 사용되지 않을 것이다.
 

실제 결과도 insert 쿼리만 수행되는 것을 확인할 수 있었다.
 
 

데이터베이스에서 조회

그럼 1차 캐시에 값이 없다면 어떻게 작동할까?

  1. find 시, 1차 캐시를 조회한다.
  2. 1차 캐시에 입력받은 id를 가진 데이터가 존재하지 않으면 DB를 조회한다.
  3. DB에서 가져온 데이터를 1차 캐시에 저장한다.
  4. 1차 캐시에 저장한 값을 return한다.

 
‘1차 캐시에서 조회’ 실험을 하며 DB에 id가 101인 Entity가 저장되었기 때문에,
이제 코드를 조금 수정한 뒤 find를 통해 DB에서 조회하는 과정을 테스트해보자
 

id가 101인 Entity를 조회하는 코드가 있을 때,
첫 번째 조회 시에 1차 캐시에 엔티티가 저장되므로 두 번째 조회 시에는 select 쿼리가 필요 없다.
 
때문에 총 1번의 select 쿼리가 수행된다는 것이다.
 

그리고 예상대로 1번의 select 쿼리만이 수행되었다.
 

💥 문제점

좋은 기술같지만, 사실 1차 캐시는 효율성을 크게 제공해주진 못한다.
1차 캐시는 10명의 요청이 들어오면 10개의 1차 캐시가 생성된다..
 
또한 요청마다 생성되는 영속성 컨텍스트는 요청이 끝나고 트랜잭션이 종료될 때
함께 사라지기 때문에 1차 캐시는 효율성 측면에서 큰 도움이 안되는 경우가 많다.
 
물론 비즈니스 로직이 굉장히 복잡한 경우엔 쿼리가 줄어들긴 하지만, 1차 캐시는 성능보다 매커니즘에서 얻는 이점이 크다.
 
반면, 하나의 트랜젝션이라는 작은 생명 주기를 가지는 1차 캐시와 다르게,
전체에서 공유하는 캐시는 ‘2차 캐시’라는 것이 있다.
 


 

2. 영속 엔티티의 동일성 보장

1차 캐시가 있기 때문에,
동일 트랜잭션 내에서는 영속 엔티티의 동일성을 보장한다.
 
 


 

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

트랜잭션을 지원하기 때문에 persist 시, DB에 보내지 않고 쌓아두다가
트랜잭션을 커밋하는 순간에 DB에 INSERT 쿼리를 보낸다.
 
 

memberA를 persist 시, 방금까지 배웠던 대로 1차 캐시에 엔티티를 저장한다.
 
하지만 동시에 엔티티를 분석하여 INSERT 쿼리를 생성 후 ‘쓰기 지연 SQL 저장소’ 라는 곳에 SQL문을 저장한다.
 
memberB를 persist 시에도 위와 같이 작동한다.
 
최종적으로 쓰기 지연 SQL 저장소에 memberA, memberB에 대한 INSERT 쿼리 2개가 저장되는 것이다.
 
 

이러한 상태에서 트랜잭션을 commit하게 되면 쓰기 지연 SQL 저장소에 있던 쿼리들이 flush 된다.

  • JPA에서 flush 된다는 것은 저장해둔 쿼리가 DB에 전송된다는 것을 뜻한다.
  • 즉, 영속성 컨텍스트의 변경 내용을 DB에 반영한다는 것이다.

그런 다음 실제 DB의 트랜잭션이 commit 된다.
 
 

이를 코드로 표현하면 위와 같다.
그런데 왜 이렇게 사용할까?

버퍼링

이렇게 값을 묶어서 한번에 사용하는 것을 버퍼링이라고 한다.
만약 바로바로 Insert 쿼리를 수행해버리면 최적화할 수 있는 여지가 사라지게 된다.
 
트랜잭션을 커밋하기 전에만 insert를 수행하면 상관없기 때문에 이렇게 동작해도 문제가 없다.
 
이때 버퍼(SQL쿼리)들을 저장해두었다가 한번에 수행 시켜주는, 버퍼링 역할을 수행하는 것을 ‘JDBC Batch’라고 한다.
 

hibernate는 설정에서 위와 같은 batch_size 옵션을 설정을 통해 한번에 커밋하는 배치의 크기 단위를 지정할 수 있다.
이렇게 버퍼를 모아서 write하는 방식을 잘 사용하면 성능적인 측면에서 이점을 얻을 수 있다.
 


 

4. 엔티티 수정 - 변경 감지(Dirty Checking)

영속성 컨텍스트에 영속된 엔티티는 어떻게 수정할 수 있을까?
바로 ‘변경 감지’를 통해 간단한 수정이 가능하다.
 

위 코드는 id가 150인 엔티티의 이름을 “A”에서 “ZZZZZ”로 변경하는 로직이다.
새로 이름은 변경했으니 persist나 update라는 메서드가 수행되어야 하는 것 아닐까?
 

물론 아니다.

 
JPA는 영속성 컨텍스트 내에 해당 엔티티가 존재하는 한에서, 엔티티에 변경이 일어나면 변경 사항을 감지하여 자동으로 반영해준다.
 

기존 150번 id의 userName 값은 “A” 였는데
 

setUsername으로 member 엔티티의 값만 변경해주었음에도 불구하고
update 쿼리가 실행되었다.
 

 

실제 DB의 값 또한 변경된 것을 확인할 수 있었다.
 
 

변경 감지 내부 로직

이러한 마법같은 일의 비밀은 영속성 컨텍스트 안에 있다.
 

  1. 트랜잭션을 커밋하는 시점에 내부적으로 flush 가 호출됨.
  2. 그 다음 엔티티와 스냅샷을 비교
    • 스냅샷이란 영속성 컨텍스트에 영속된 가장 초기의 상태를 기록해놓은 것
    • 그러한 스냅샷을 통해 엔티티에 변경사항이 있는지 비교
  3. 만약 엔티티에서 변경 사항이 발견된다면 '쓰기 지연 SQL 저장소'에 update 쿼리를 추가
  4. DB에서 쿼리가 실행되며 update 된 값이 반영

 


 

5. 엔티티 삭제

삭제는 그냥 find로 엔티티를 찾아와서 remove 해주면 끝이다.
 


플러시

플러시란 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것이다.
다시 말해, 영속성 컨텍스트와 데이터베이스 값의 상태를 일치시키는 작업이다.

‘쓰기 지연 SQL 저장소’에 저장되어 있던 SQL 쿼리들을 DB에 요청함으로서 수행된다.
 

플러시 발생 후, 동작

  1. 변경 감지
  2. 수정된 엔티티에 대한 update 쿼리 ‘쓰기 지연 SQL 저장소’ 에 등록
  3. ‘쓰기 지연 SQL 저장소’의 쿼리를 데이터베이스로 전송 (등록, 수정, 삭제 등)

 

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

  • em.flush() - 직접 호출
  • 트랜잭션 커밋 - 플러시 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출

 

em.flush() 예시

기존에 flush를 따로 수행하지 않았을 때라면 “==============”가 SQL 쿼리 수행 이전에 출력 되겠지만,
 
위와 같이 트랜잭션이 commit 되기 전에 직접 영속성 컨텍스트를 flush 해주는 상황에선 SQL 쿼리 수행 후 “==============”가 출력된다.
 


 

🙋🏻‍♂️ 그럼 flush 사용 후엔, 1차 캐시가 지워지나요?

아니다.
flush는 오직 변경 감지와, 쓰기 지연 SQL 저장소에 있는 쿼리를 DB로 전송하는 것이고
트랜잭션이 종료되는 것이 아니다.
 


 

⁉ JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유

JPQL은 자동으로 번역되어 SQL 저장소에 저장되는 것이 아니라,
DB에서 바로 수행이 된다는 특징을 가지고 있다.
 
이러한 특징을 가진 JPQL 사용 시,
위 코드와 같이 DB에 등록된 Member가 0개라고 가정했을 때

persist로 여러 member를 등록한 뒤 JPQL로 조회했을 때 insert 쿼리는 트랜잭션이 커밋되는 시점에서 실행되기 때문에 아무런 조회 결과도 얻을 수 없다.
 
이렇듯 쿼리 수행 시점의 차이로 인해 발생할 수 있는 다양한 문제를 막기 위해 JPQL 실행 시, 자동으로 flush 되도록 만들어 놓은 것이다.
 


 

🙋🏻‍♂️ JPQL 쿼리 실행시 플러쉬 호출로 인해 얻는 이점이 없어요ㅠ

만약 JPQL이 persist되는 엔티티와 전혀 관계없는 쿼리문이라고 한다면,
오토 플러쉬로 얻을 수 있는 이점이 없다.
 
이런 상황에서는 flushMode를 Commit으로 변경하여 커밋할 때만 플러시되도록 규칙을 수정할 수 있다.
 
 


📚︎ 플러시 정리!

  • 플러시를 수행한다고 해서 영속성 컨텍스트를 비우는 것이 아님
  • 그저 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 작업만 수행
  • 트랜잭션이라는 작업 단위가 중요 → DB와 영속성 컨텍스트는 트랜잭션 커밋 직전에만 동기화 하면 됨

 


✂︎ 준영속 상태

  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

 

준영속 상태로 만드는 방법

 
 

detach(entity) 예시

detach 메서드를 통해 member를 준영속 상태로 전환하면

 

 
아까는 변경 감지에 의해 update 쿼리가 실행되었어야 했을 코드가 select 쿼리만 수행하는 것을 볼 수 있다.
 
이 외에도 영속성 컨텍스트를 초기화 하거나, 종료하는 방식으로 엔티티를 준영속 상태로 전환할 수 있다.

728x90
반응형