JPA 영속성 관리

Posted on April 22nd, 2022

시작

JPA 영속성 관리에 대해서 한번 정리해볼 필요가 있었다. 🤔

EntityFactory EntityManager

우선 팩트부터 얘기하겠다. 엔티티 매니저 팩토리는 엔티티 매니저를 만든다.JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀도 만든다. 엔티티 매니저는 트랜잭션을 시작할 때, 데이터베이스 연결에 꼭 필요한 커넥션을 획득한다.

이러한 팩토리는 비용이 많이 들기에 하나의 애플리케이션 전체에서 공유하도록 설계되어 있다. 하지만 매니저는 비용이 적기 때문에 많이 만들어도 무방하다.

그럼 여기서 생각해보자. 팩토리는 1개고 매니저는 여러개다. 이러한 설계 구조에서 여러 스레드가 있다. 팩토리는 여러 스레드가 공유를 동시에 할 수 있고 매니저는 동시성 문제로 스레드 간에 절대 공유를 할 수 없다.

그럼 너무나도 당연한게 여러 스레드가 1개의 팩토리에 동시에 접근해도 괜찮게 설계가 되어 있을 것이며 반대로 매니저는 여러 스레드가 한번에 공유를 하면 안되지 않는가? 또한 어떻게 보면 그게 당연하다. 비용이 적게 들기 때문에.

나는 뭐.. 이렇게 외운다. 👀

이제 여기서 엔티티 매니저는 엔티티를 저장, 수정, 삭제 그리고 조회의 기능을 갖고 있고 이러한 엔티티관련 일들을 모두 처리한다. 즉, 엔티티를 관리하는 관리자라고 생각하면 된다. ( + 트랜잭션을 관리하는 트랜잭션 관리자도 따로 있다 )

( + 추가 ) 커넥션풀은 하나의 기술일 뿐인데 이 커넥션을 관리하고 저장되는 부분을 WAS 메모리상에 있다고 알고 있다. 잘못 알고 있으면 댓글 부탁좀..

영속성 컨텍스트

그림을 보면 간단하게 이해할 수 있을 것 이다. 엔티티 매니저가 여러 엔티티를 영속성 컨텍스트(엔티티를 영구 저장하는 환경) 라는 저장소에 persist라는 기술을 사용해서 저장하고 관리하는 것이 그림의 해석이다.

이러한 영속성 컨텍스트는 엔티티 매니저와 1:1 관계다. 즉, 엔티티 매니저가 만들어지면 영속성 컨텍스트도 하나 만들어진다.

엔티티의 생성 주기

이것 또한 그림을 보면 간단하게 이해할 수 있다. 엔티티가 영속성 컨텍스트 안에 존재한다면 영속, 만약 이 엔티티가 영속성 컨텍스트에서 나왔다면 준영속 그리고 처음부터 아예 속해있지 않았다면 비영속이다.

그림에 없는 마지막 삭제상태인 경우는 어디에도 없는 (데이터베이스도 포함) 상태인 경우를 삭제라고 한다.

엔티티 식별자 그리고 1차 캐시

이 그림 이전에는 각 엔티티가 어떤 엔티티인지 구분을 할 수 없었다. 좀 더 명확하게 그림을 그려보자면 영속성 컨텍스트 안에 있는 엔티티들은 각 키가 있고 그 식별자 값은 데이터베이스 기본 키와 매핑이 되어있다. 즉, JPA 에서 @Id에 쓰이는 그 칼럼이 키라는 뜻이다.

1차 캐시는 굉장히 중요한 개념이다. 만약 Entity Manager를 통해서 ( em.find() ) 어떠한 특정한 엔티티를 찾고자 할 때, 1차 캐시에서 식별자 값으로 엔티티를 찾기 때문에 존재 한다면 데이터베이스에 갈 필요가 없다. 즉, 조회 성능이 좋아진다는 뜻이다. 만약, 존재하지 않다면 데이터베이스에서 엔티티를 추출하고 1차캐시에 저장 후, 영속 상태의 엔티티를 반환한다.

영속 엔티티의 동일성

em.find를 통해서 영속 엔티티를 반환하는 데 만약 2번의 호출로 두 개의 동일한 엔티티를 부른다면 과연 그 엔티티가 같은 엔티티 일까?

Person personA = emfind(Person.class, "person1");
Person personB = emfind(Person.class, "person1");

personA == personB // true 

이유를 묻는다면 간단하다. 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다고 말하면 된다. 그 이상 묻는다면 뭐....👀

쓰기 지연 SQL 저장소

가장 중요한 문장이 있다. 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 그리고 트랜잭션을 커밋할 때, 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭셔을 지원하는 쓰기 지연이라고 한다.

이 두문장이 굉장히 중요하다. 우선 여기서 커밋이라는 것을 알아야 한다. ( flush와 헷갈릴 수 있으니 같이 정리 )

커밋 : 트랜잭션 작업이 성공적으로 끝났고 데이터베이스가 일관된 상태가 있을 때 트랜잭션 관리자에게 알리는 것.

프러쉬 : 영속성 컨텍스트에 있는 엔티티 정보를 DB에 동기화하는 것.

즉, 엔티티 매니저가 데이터베이스가 '이제 일관된 상태에 있어요!'(프러쉬) 라고 말하기 전까지 영속성 컨텍스트는 쓰기 지연 SQL 저장소에 있는 여러 SQL을 모아두고 있는다. 그리고 이제 엔티티 매니저가 Flush를 하여 이 저장소에 있는 SQL을 전부 데이터베이스에 적용을 시키고 엔티티 정보를 동기화하였을 때 그제서야 엔티티 매니저는 커밋을 한다. 그러니까 무조건 커밋하기 전에 Flush를 해야하는 게 맞는거다. 엔티티 매니저는 우선 트랜잭션을 커밋하면 영속성 컨텍스트를 Flush 한다. 이 문장은 매우 옳다.

엔티티 수정

우선 우리는 어떻게 테이블의 row를 수정하는가? 만약 Member라는 테이블에 id가 1인 row의 name을 수정하고자할 때, 우리는 'update member set name = ? where id = 1' 라는 쿼리문을 작성할 것이다. 만약 한개가 아니라 두개면 ? 'update member set name = ?, age = ? where id = 1' 라는 쿼리문을 작성할 것이다.

왜? 한번에 다 하면 되는거 아닌가? 라는 의문을 품을 수 있는데. 실수로 그 필드를 입력을 안할 수 있기 때문에 쿼리문을 여러개 만들어서 관리를 한다. 이 '실수'라는 것이 막상 겪어보지 못하면 느낌이 안올 수 있다. 하나의 예를 들면 Builder에 대한 필요성을 못느끼는 것과 같달까... 무튼 이러한 여러 쿼리를 만들기 때문에 우리는 SQL에 의존하게 된다.

그럼 어떻게 해야하는가 ??

Dirty Checking - 변경 감지

우선 팩트부터 얘기하자면 JPA의 기본 전략 즉, default는 엔티티의 모든 필드를 업데이트한다는 것이다. 쿼리가 여러개가 아니라 우리가 실수할 수도있다는 그 모든 필드에 대한 하나의 쿼리만 존재한다는 것이다. 다만, 엔티티를 수정하는 것이므로 SQL 처럼 실수할 가능성은 현저히 줄어든다.

나는 개인적으로 dirty checking 이라는 말을 좋아하지는 않는다. 그냥 변경하는 것을 감지해준다. 누가 ? JPA가 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장( 스냅샷 )해두고 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.

그러니까 영속성 컨텍스트에 @id, Entity 뿐만 아니라 스냅샷 이라는 필드도 있다는 것이다. 당연히 그러면 이 엔티티가 영속 상태의 엔티티만 해당이 되는 것이다.

그럼 엔티티 수정에 대한 결론을 내보자. 모든 필드를 업데이트 하기 때문에 전송량에 있어서 증간하는 단점은 있지만 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성하며 수정 쿼리가 1개이고 항상 같다는 것과 재사용의 장점이 있다.

추가적으로 더 말하자면, 이건 default 기본 전략이며 DynamicUpdate 와 같이 동적으로 Update를 할 수 있는 방법도 있다.

FLUSH - 프러쉬

여기서 다시 한번 개념을 바로잡고자 한다.

커밋 : 트랜잭션 작업이 성공적으로 끝났고 데이터베이스가 일관된 상태가 있을 때 트랜잭션 관리자에게 알리는 것.

프러쉬 : 영속성 컨텍스트에 있는 엔티티 정보를 DB에 동기화하는 것.

즉, 간단하게 생각하면 영속성 컨텍스트라는 공간안에 엔티티가 수정이 됬으며 또 새로운 엔티티가 들어왔으면 이 영속성 컨텍스트 뿐아니라 Data base 공간에도 똑같이 영속성 컨텍스트와 동기화를 해줘야 한다.

플러시 하는 방법은 3가지가 있다.

  • em.flush()
  • 트랜잭션 커밋 시 플러시가 자동 호출
  • JPQL 쿼리 실행 시 플러시가 자동 호출 ( JPQL : JPA의 일부로 정의된 플래폿. 독립적인 객체지향 쿼리 언어 )

근데 여기서 의문이 드는 것은 왜 자동일까? 너무나도 당연한게 만약 동기화를 해주지 않았다면 당연히 DB에서 Entity를 갖고올 때 그 값이 과연 조회가 될까? 당연히 안되겠지.

간단 정리

우선 팩트만 하나 말하고 가자면, 지연로딩이라는 것은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법.

영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 이러면 좋은 점은 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능(프록시)을 사용할 수 있다는 것이다.

이러한 기능들은 꼭 ! 영속상태여야만 한다.

일반적으로 트랜잭션을 커밋할 때 영속성 컨텍스트가 플러시 된다.

++ Flush 후, DB 반영 결과

flush를 하고 난 후, 영속성 컨텍스트에 있는 SQL을 모두 날렸다. 하지만, DB에 보면 반영이 되지 않았다. 이유는 Commit 이다. Commit에 대한 내용을 좀 더 추가하자면 모든 부분작업이 정상적으로 완료하면 이 변경사항을 한꺼번에 DB에 반영합니다.

'스터디가 끝난 후, 알아보고 다시 단톡에 뿌린 내용.' Flush를 했고 Commit을 안한상태에서 DB에 봤을 때 반영이 되지 않은 이유는 Commit에 대한 정의가 조금 부족했다 생각하는데 좀 더 내용을 추가하자면 Commit : 모든 부분작업이 정상적으로 완료하면 이 변경사항을 한꺼번에 DB에 반영합니다. 그리고 아는 DBA분께 여쭤보니 '디비블록에다가 디스크에 써도 된다고 표시해주는 행위' 그게 곧 Commit이다. 라고 하시는 거보니까 Flush 는 동기화를 위해 DB에 우선적으로 SQL을 날려주는 것이고 그 SQL을 반영을 하면 동기화가 되는 것이며, 반영은 아직 하지 않은 상태.


참고 : 자바 ORM 표준 JPA 프로그래밍 - 김영한