시뻘건 개발 도전기

Entity와 영속성 #2 본문

API/JPA

Entity와 영속성 #2

시뻘건볼때기 2020. 11. 29. 19:07
반응형

그렇다면 영속성 컨텍스트가 왜 필요한지 알아보자.

 

엔티티 조회

  영속성 컨텍스트는 내부에 1차 캐시라고 하는 녀석을 가지고 있다. 영속 상태의 모든 엔티티는 이 1차 캐시에 저장된다. 이해하기 쉽게 코드와 그림을 보자.

Member member = new Member();
member.setId = "dotori";
em.persist(member);

Member dotori = em.find(Member.Class, "dotori"); // 조회

[그림 1] 1차 캐시 예시

1차 캐시 내에 @Id로 맵핑된 필드 값을 식별자를 가지고 엔티티가 저장된다. (아직 데이터베이스에 저장된 상태는 아니다.)

  조회를 하게 되면 엔티티 매니저는 1차 캐시에서 해당하는 엔티티를 찾는다. 만약 1차 캐시에 없다면 데이터베이스에서 조회한다. (JPA란?에서 JPA의 장점 중 '성능 문제 해결' 내용의 확장이라고 할 수 있다.) 만약 데이터베이스에서 조회할 때라면 엔티티 매니저는 조회한 엔티티를 반환하기전에 1차 캐시에 저장한다. (이 때는 영속상태겠죠?) 그렇기 때문에 데이터베이스로부터 조회한 엔티티는 항상 영속 상태이다.

 

엔티티 등록

  엔티티를 등록하는 코드를 먼저 보자.

EntityManager em = emf.createEntiryManager();
EntityTransaction tx = em.getTransaction();

tx.begin();

em.persist(member1);
em.persist(member2);

tx.commit(); // INSERT SQL 실행

엔티티 매니저는 트랜잭션을 커밋하기 전까지 내부 쿼리 저장소에 SQL을 쌓아둔다. 그러다가 트랜잭션이 커밋혹은 앤티티 메니저의 flush를 하게되면 쌓아둔 모든 SQL을 데이터베이스에 보내게된다. 이를 "쓰기 지연"이라고 한다. flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이 때 CRUD한 엔티티를 데이터베이스에 반영한다.

  트랜잭션을 지원하는 쓰기 지연이 가능한 이유에 대해서 알아보자.

begin

save(A)
save(B)
save(C)

commit

1번째 방법 - save를 할 때마다 바로 바로 데이터베이스에 등록 쿼리를 보낸 뒤 마지막에 commit을 한다.

2번째 방법 - save를 할 때마다 메모리에 SQL를 모아놓고 commit을 할 때 모아둔 SQL을 한 번에 보낸다.

두 가지 방법 모두 결과는 같을 것이다. 쿼리를 그때 그때 보내 데이터베이스에게 전달해도 트랜잭션을 커밋하지 않으면 소용이 없을 것이다. 어쨌든 커밋 이전에만 쿼리를 전달하면 된다. 이것이 쓰기지연이 가능한 이유다.

 

엔티티 수정

  SQL을 직접 작성하여 전달하게 되면 유지보수하면서 점점 추가되고 커질 수 있다.

회원의 이름과 나이를 업데이트하는 쿼리다.

UPDATE member
SET name=?, age=?
WHERE id=?

회원의 등급을 변경하는 기능이 추가되면 다음과 같은 쿼리가 추가될 것이다.

UPDATE member
SET grade=?
WHERE id=?

혹은 하나로 합쳐서 수정될 수도 있다.

UPDATE member
SET name=?, age=?, grade=?
WHERE id=?

 

하나로 합친 쿼리를 사용했을 때 실수로 이름, 나이, 등급 중 한 가지라도 입력하지 않았다면 어떨까? 결국엔 이러한 부담감을 덜고자 첫 번째 방법과 같이 쿼리를 추가하여 사용할 수 있다는 것이다. 우린 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.

  JPA는 변경 감지를 통해 수정된다. 다음 코드를 보자.

EntityManager em = emf.createEntiryManager();
EntityTransaction tx = em.getTransaction();

tx.begin();

Member dotori = em.find(Member.class, "dotori");

dotori.setAge(28);
dotori.setUserName("dotori");

em.update(dotori); // persist()처럼 업데이트하는 메소드가 필요하지 않을까?

tx.commit();

아쉽게도 update하는 메소드는 없다. JPA는 엔티티의 데이터만 변경해도 데이터베이스에 반영이 된다. 바로 변경 감지라는 기능이 있기 때문이다.

[그림 2] JPA의 변경 감지

1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush()가 호출 된다.

2. 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.

3. 변경된 엔티티가 있다면 수정 쿼리를 생성하여 SQL 저장소에 저장한다.

4. SQL 저장소의 SQL을 데이터베이스에 전달한다.

5. 데이터베이스 트랜잭션을 커밋한다.

 너무나도 당연하겠지만 이 변경 감지 기능은 영속 컨텍스트에 의해 관리되는 엔티티에게만 적용 된다. 변경 감지로 인해 변경된 쿼리는 위에서 언급했던 하나로 합친 쿼리를 사용한다.(JPA의 기본 전략) 이는 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다. 또한 데이터베이스는 이전에 파싱된 쿼리를 재사용할 수도 있다. 만약 수정된 데이터만 사용해서 동적으로 SQL을 생성하고 싶다면 DynamicUpdate라는 하이버네이트 확장 기능을 사용해야한다.

@import org.hibernate.annotations.DynamicUpdate;

@Entoty
@DynamicUpdate
public class Member {
	// ...
}

 

엔티티 삭제

  엔티티를 삭제하려면 먼저 삭제할 엔티티를 조회해야한다.

Member dotori = em.find(Member.Class, "dotori");
em.remvoe(dotori);

동작 원리는 [그림 2]와 같다. remove()를 호출하면 영속성 컨텍스트에서도 제거된다. 그렇기 때문에 재사용하지 않고 가비지 컬렉션의 대상이 되도록 남겨두자.

반응형

'API > JPA' 카테고리의 다른 글

Entity 매핑 #1  (0) 2021.01.13
Entity와 영속성 #3  (0) 2020.11.29
Entity와 영속성 #1  (0) 2020.11.29
JPA를 사용하기 위한 설정과 동작 원리  (0) 2020.09.29
JPA란?  (0) 2020.09.24
Comments