저번 게시물에서 영속성 관리에 대해 이야기 하면서 마지막에 특징이자, 장점을 간단히 보고 넘어갔습니다. 이번에는 각각을 좀 더 자세히 알아보겠습니다.
* 1차 캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있는데, 이것을 1차 캐시라고 합니다. 영속 상태의 엔티티는 @Id 로 매핑한 식별자를 키 값으로 모두 이곳에 저장이 됩니다.
Member member = new Memner();
member.setId("member1");
member.setUsername("name1");
em.persist(member);
위의 코드를 실행하면 아래의 그림처럼 1차 캐시에 Member 엔티티를 저장합니다. 앞에서 이야기가 나왔던 flush 가 일어나지 않았으므로 실제 데이터베이스에는 저장되지 않은 상태입니다.
1차 캐시의 키는 식별자 값입니다. 그리고 식별자 값은 데이터베이스의 기본 키에 메핑되어 있습니다.
Member member = em.find(Member.class, "member1");
이렇게 find() 메소드를 이용해서 엔티티를 조회해봅시다. 첫번째 파라미터는 엔티티의 클래스 타입이고, 두번째 값은 식별자 값입니다.
em.find() 를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 없다면 데이터베이스에서 조회해옵니다.
Member member2 = em.find(Member.class, "member2");
이렇게 1차 캐시에 없는 엔티티를 조회해보면 다음 그림과 같습니다.
위와 같은 일이 발생하고, 데이터베이스에서 조회한 member2 는 1차 캐시에 저장하고 반환하게 됩니다. 그러면 member2 도 managed 상태, 즉 영속 상태가 됩니다.
위의 그림을 보다보면 알 수 있는 것이 있습니다. 조회하고자 하는 엔티티 데이터가 캐시에 존재하면 그것을 꺼내주죠. 즉 반복해서 호출해도 컨텍스트는 늘 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다는 것입니다.
이것을 통해서 나타나는 다음 장점이 있습니다.
* 동일성 보장
영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하므로 나타나는 특징입니다. 이를 통해서 영속성 컨텍스트는 성능상의 이점과 동일성을 보장하게 됩니다.
동일성이 무엇인지 헷갈릴 수 있어 잠깐 짚고 넘어가자면, '==' 비교를 생각해보시면 됩니다. Java 에서 인스턴스끼리 '==' 비교를 하면 주소값이 같아야 참이 나오는 것은 다들 아실겁니다. 이것을 동일하다고 표현합니다. 실제 인스턴스가 같은 것이죠. 자바는 Call by Value 니깐 참조하고 있는 주소값이 같은 것을 통해 동일한 인스턴스란 것을 알 수 있습니다.
이것과 반대되는 개념은 동등성인데요, 이것은 인스턴스가 가지고 있는 값이 같은 것을 말합니다. 엔티티나 도메인 객체를 구현할때, equals() 와 hashCode() 를 많이 구현하실 텐데요, 여기서 equals() 가 값을 비교해주죠. 그래서 인스턴스끼지 equals() 로 비교하면 동등한가를 비교하게 되는 동등성을 말합니다.
* 트랜잭션을 지원하는 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
Member memberA = new Member();
memberA.setId("member1");
memberA.setUsername("name1");
...
em.persist(memberA);
em.persist(memberB);
transaction.commit();
위와 같이 코드를 실행한다고 해보면, 마지막에 트랜젹션을 커밋하기 전에 인스턴스들이 persist() 로 영속상태에 들어가는 것을 확인할 수 있습니다. 그러면 트랜잭션이 커밋될 때 어떤 일이 일어나는지 보겠습니다.
위의 과정이 차례대로 일어나게 됩니다.
첫번째 그림을 보면 memberA 가 영속화 됐고, 1차 캐시에 엔티티를 저장하면서 동시에 회원 엔티티 정보로 등록쿼리를 만듭니다. 그리고 쿼리를 '쓰기 지연 SQL 저장소' 에 보관합니다.
그 후, memberB가 영속화 되고, 마찬가지로 1차 캐시에 저장하면서 동시에 '쓰기 지연 SQL 보관소' 에 등록 쿼리를 보관합니다.
마지막으로 트랜젹션이 commit 되면 영속성 컨텍스트의 내용을 flush 합니다. flush 는 영속성 컨텍스트의 내용을 DB 에 반영하는 것입니다.
이와 같이 쓰기지연이 가능한 이유는 한 트랜잭션 범위 안에서 실행되는 것이기 때문입니다. 커밋이 되기전에만 데이터베이스에 SQL 을 전달한다면 아무 문제가 없죠. 이 쓰기지연의 장점은 데이터베이스 테이블 row 에 lock 이 걸리는 시간을 최소화한다는 점인데, 이것은 나중에 정리하도록 하겠습니다.
* 변경감지
Member memberA = em.find(Member.class, "memberA");
memberA.setUsername("chanegedName");
memberA.setAge(29);
transaction.commit();
위와 같은 코드를 실행하면 어떻게 될까요?
위의 그림을 보겠습니다. JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해둡니다. 이것을 "스냅샷" 이라고 부릅니다. 그 후 flush 되는 시점에 스냅샷과 엔티티를 비교하여 변경된 엔티티를 찾습니다. 그 후에는 '쓰기 지연 SQL 저장소' 에 update 쿼리를 만들어 저장하고 저장소의 SQL 을 데이터베이스에 보내고, 커밋을 하는 과정이 일어납니다.
변경감지는 당연하게도, 영속성 컨텍스트가 관리하는 영속상태(managed)의 엔티티에만 적용이 됩니다. 만약 detach로 준영속 상태로 만들었다거나 비영속 상태인 엔티티는 값을 변경해도 데이터베이스에 반영되지 않습니다.
여기서 변경감지로 작성된 update 쿼리를 잠깐 살펴보겠습니다.
UPDATE MEMBER
SET
NAME=?,
AGE=?
WHERE
id=?
먼저 예상되는 쿼리는 이런 모양입니다. 이름과 나이만 바꿨기 때문이죠. 하지만 JPA 가 만드는 쿼리는 다릅니다.
UPDATE MEMBER
SET
NAME=?,
AGE=?,
GRADE=?,
...
WHERE
id=?
예상과는 다르게 모든 필드를 업데이트 해주네요. 예상을 하실 수 있겠지만 좀 거슬러 올라가보겠습니다.
PreparedStatement 라는게 혹시 기억이 나실지요..? 동적 쿼리를 만들때 우리는 원래 저것을 사용했습니다. 하나의 같은 쿼리안에 동적으로 파라미터들을 채워넣었었죠. 이렇게 했을 때의 장점이 있었습니다. 데이터베이스가 쿼리를 파싱하는 것이죠.
JPA 의 기본전략도 마찬가지입니다. 데이터베이스는 동일한 쿼리라고 인식하면 이전에 한 번 파싱된 쿼리를 재사용합니다. 모든 필드에 대한 update 쿼리를 만들어두고, 값만 동적으로 바뀐다면 언제나 같은 쿼리가 나가게 됩니다. 하지만 바뀌는 필드에 대한 update 문을 쿼리로 작성한다면 매번 다른 쿼리를 만들면서 재사용을 못하게 되겠죠.
물론 필드가 너무 많거나, 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 생성하는 전략을 선택할 수도 있습니다. @DynamicUpdate 를 사용한다면 수정된 데이터만 사용해서 동적으로 sql 을 생성합니다. 이는 환경에 맞게 선택하시면 되겠습니다.
마지막 특징이자 장점인 '지연 로딩' 에 대한 내용은 후에 프록시 관련 내용까지 정리가 되면 다루도록 하겠습니다. 👍
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 양방향 연관관계 & 연관관계의 주인 (1) (0) | 2022.05.09 |
---|---|
[JPA] 연관관계 매핑 : 기본 (2) (0) | 2022.05.08 |
[JPA] 연관관계 매핑 : 기본 (1) (0) | 2022.05.08 |
[JPA] 영속성 관리 (0) | 2022.05.03 |
[JPA] JPA 란? 🤔 (0) | 2022.05.02 |