- 참조: 자바 ORM 표준 JPA 프로그래밍
- 영속성 컨텍스트
영속성 컨텍스트는 엔티티를 식별자 값(@Id - 기본 키) 으로 구분하므로 영속 상태는 식별자 값이 반드시 있어야 한다.
영속성 컨텍스트의 엔티티는 커밋하는 순간 DB에 반영하는데 이를 플러시(flush) 라고 한다.
영속성 컨텍스트는 아래 기능을 제공한다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
- 엔티티 조회
영속성 컨텍스트는 내부에 캐시를 갖고 있는데 이를 1차 캐시라고 한다. 영속 상태의 엔티티는 모두 이곳에 저장되며, @Id 로 맵핑한 식별값을 이용하여 구분한다.
String id = "ID#1";
Member member = new Member();
member.setId(id);
member.setUserName("ocwokocw");
member.setAge(33);
em.persist(member);
위 코드를 실행하면 member 를 1차 캐시에 저장한다. 마치 map 에서 ("ID#1", member 인스턴스) 와 같이 key를 @Id 식별값으로, value 는 인스턴스 로 저장하는 구조이다.
Member findMember = em.find(Member.class, id);
위와 같이 EntityManager 의 find 연산을 수행할 때, id 값을 이용하여 영속성 컨텍스트에서 엔티티를 찾는다. DB 에 find 연산이 도달하기 전에 영속성 컨텍스트 안에서 id 에 해당하는 엔티티가있으면 곧바로 가져올 수 있으므로 "1차 캐시" 가 된다.
만약 1차 캐시에 없으면 DB 에서 가져온다. 이 때 단순하게 가져오는게 아니라 DB 에서 조회하면서 생성된 엔티티를 영속성 컨텍스트의 1차 캐시에 저장을 하고 반환한다.
글 도입부의 영속성 컨텍스트 특징에서 동일성을 보장한다고 했는데 이게 무슨말일까?
Member findMember1 = em.find(Member.class, id);
Member findMember2 = em.find(Member.class, id);
if(findMember1 == findMember2) {
System.out.println("Same identity.");
}
위의 코드를 보고 결과를 예측할 때, 엔티티 매니저의 동작을 모른다면 findMember1 과 findMember2 는 다른 인스턴스의 주소를 갖기 때문에 == 비교는 false 를 반환한다고 생각할것이다. 하지만 같은 id 로 EntityManager 에서 find 연산을 하면 동일한 엔티티를 반환하므로 findMember1과 findMember2의 동일성을 보장해준다.
대부분 알고 있겠지만 equals() 연산인 동등성과 헷갈리면 안된다. 동일성(==)과 동등성(equals)의 자세한 내용은 effective java의 euqals 메소드 재정의 부분을 참조하길 바란다.
- 엔티티 등록
여태까지 예제를 수행하면서 콘솔 로그를 꼼꼼히 읽어보았다면 이상한점을 하나 발견했을 것이다.
Member member1 = new Member();
member1.setId("ID#1");
member1.setUserName("ocwokocw1");
member1.setAge(31);
em.persist(member1);
System.out.println("Persist member1.");
Member member2 = new Member();
member2.setId("ID#2");
member2.setUserName("ocwokocw2");
member2.setAge(32);
em.persist(member2);
System.out.println("Persist member2.");
Member member3 = new Member();
member3.setId("ID#3");
member3.setUserName("ocwokocw3");
member3.setAge(33);
em.persist(member3);
System.out.println("Persist member3.");
위와 같이 중간마다 sysout 을 찍으면서 persist 로 영속화 시킬 때, Insert 쿼리로그는 항상 마지막에 찍힌다는 사실이다.
Persist member1.
Persist member2.
Persist member3.
Hibernate:
/* insert com.example.demo.member.Member
*/ insert
into
MEMBER
(age, NAME, ID)
values
(?, ?, ?)
Hibernate:
/* insert com.example.demo.member.Member
*/ insert
into
MEMBER
(age, NAME, ID)
values
(?, ?, ?)
Hibernate:
/* insert com.example.demo.member.Member
*/ insert
into
MEMBER
(age, NAME, ID)
values
(?, ?, ?)
Log가 위에처럼 나타나는것은 도입부에서 영속성 컨텍스트의 특징 중 "트랜잭션을 지원하는 쓰기 지연" 과 관련이 있다. 영속성 컨텍스트는 member1, member2, member3 를 영속화 시킬 때, 곧바로 DB에 반영하는게 아니라 쓰기 지연 SQL 저장소에 Insert 쿼리들을 하나씩 쌓는다.
그러다가 트랜잭션 커밋 메소드를 호출하면 영속성 컨텍스트의 변경 내용(CUD)을 DB와 동기화 하는 플러시를 수행한 후에 실제 DB의 트랜잭션을 커밋한다.
persist 를 할 때마다 SQL을 생성하여 곧바로 DB에 반영을 하던지 커밋 직전에 플러시를 통해서 한꺼번에 생성해서 DB에 반영을 하던지 결국 트랜잭션 내에서만 수행하면 결과는 동일하기 때문에 "트랜잭션을 지원하는 쓰기 지연" 이 가능하다.
- 엔티티 수정
Mybatis 를 사용해서 Update SQL 를 만들어본 경험이 있다면 Update SQL을 작성하는게 얼마나 귀찮은지 공감할것이라 생각한다.
UPDATE MEMBER
SET
AGE = #{AGE},
NAME = #{NAME}
WHERE
ID = #{ID}
위와 같이 SQL 만 작성하면 끝 아닌가? 라고 생각할 수 있지만 만약 나이만 수정해야 하는데 NAME 값을 넘기지 않으면 난감한 상황이 펼쳐진다. 그럼 아래와 같이 작성하면 어떨까?
UPDATE MEMBER
SET
ID = #{ID}
<if test="Comparator.isNotEmpty(AGE)">
,AGE = #{AGE}
</if>
<if test="Comparator.isNotEmpty(NAME)">
,NAME = #{NAME}
</if>
WHERE
ID = #{ID}
위와 같이 작성한 경우 NAME 필드에 NULL 을 할당해야 하는 경우라면 이 역시 난감해진다. 그래서 결국 어플리케이션의 동작에 따라 SQL 을 분기해야하는 상황이 생긴다. JPA 는 "변경 감지" 를 지원하는데 아래 코드를 살펴보자.
Member member1 = new Member();
member1.setId("ID#1");
member1.setUserName("ocwokocw1");
member1.setAge(31);
em.persist(member1);
member1.setAge(32);
Member findMember = em.find(Member.class, "ID#1");
System.out.println("findMember: " + findMember);
위에서 member1.setAge(32) 부분의 코드를 보면 member1의 나이를 변경하고서 update 메소드를 호출하는것과 같은 부분이 없다. 그런데 결과를 보면 아래와 같이 32로 잘 변경된다.
findMember: Member [id=ID#1, userName=ocwokocw1, age=32]
무슨 기준으로 변경을 감지할까? JPA 가 변경을 판단하여 DB에 반영하는 과정은 아래와 같다.
- 엔티티를 영속성 컨텍스트에 보관시 최초 상태를 복사한 스냅샷을 기억해 둠.
- 트랜잭션을 커밋하면 엔티티 매니저에서 플러시 호출
- 엔티티 <-> 스냅샷 비교
- 변경분에 대한 수정쿼리를 작성하여 쓰기 지연 SQL 저장소에 보관
- SQL을 DB 에 보내고 DB 트랜잭션 커밋
위의 절차에서 1번째로 언급한 절차는 상당히 중요하다. 변경분을 감지해야되니 왜 당연한 소리를 하느냐라고 생각할 수 있지만, 영속성 컨텍스트에 보관시 스냅샷을 뜬다는 말은 영속성 컨텍스트에서 매니지 상태인 엔티티만 "변경 감지"가 적용된다는 뜻이다.
생성된 UPDATE SQL은 수정한 필드에 대해서만 적용될까? JPA의 기본전략은 모든 필드에 대해서 UPDATE SQL 을 작성한다. 이유가 있을까?
DB가 SQL을 수행할때에는 구문을 파싱하여 실행계획을 세우는 작업과 파라미터를 바인딩하는 작업이 있다. 이때 실행할 때 마다 SQL 이 달라지면 파싱과 파라미터 바인딩을 둘 다 수행해야 한다. 하지만 같은 SQL을 실행하는데 파라미터만 달라지면 파라미터 바인딩만 재수행한다.
디비 컬럼수에 따라 전체 필드가 아닌 변경분의 필드로만 SQL 을 작성하는게 더 빠를 수 있지만 이는 시스템의 상황에 따라서 더 빨라지는 필드의 수가 달라질 수 있으므로 속도에 이슈가 있다면 테스트 후 사용해야 한다. 이 경우 @Entity 어노테이션 위치에다가 @DynamicUpdate 어노테이션을 추가하면 된다.
- 엔티티 삭제
엔티티 삭제도 삽입, 수정과 비슷하게 커밋 -> 플러시 -> 쓰기 지연 SQL 저장소에 SQL 구문 작성 -> 디비 트랜잭션 커밋수행과 같은 절차를 거친다.
다만 엔티티 매니저의 remove 메소드로 지운 엔티티는 영속성 컨텍스트에서 제거되는데, 이때 가비지 컬렉션의 대상이 되도록 재사용하지 않는것이 좋다.
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 영속성 관리 - 준영속과 병합 (0) | 2021.06.24 |
---|---|
JPA - 영속성 관리 - 플러시(flush) (0) | 2021.06.24 |
JPA - 영속성 관리 - 엔티티의 생명주기 (0) | 2021.06.23 |
JPA - 엔티티 매니저, 트랜잭션, JPQL (0) | 2021.06.23 |
JPA - 환경구축 및 객체맵핑, persistence.xml (0) | 2021.06.23 |
댓글