일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- java
- backend
- GithubActions
- 동시성
- 카카오 화재
- Jenkins
- 리팩터링
- 트랜잭션
- Spring Data Redis
- lazyloading
- B+TREE
- JPA
- 주문
- 부하테스트
- nonclustered index
- Metaspace
- CaffeineCache
- 제네릭
- ci/cd
- 상태패턴
- JAVA8
- Ehcache
- 지연로딩
- JDK14
- 웹캐시
- Redis
- method area
- springboot
- 재고 시스템
- 공변
- Today
- Total
NDM
[JPA] Spring Data Jpa의 Save와 Update 본문
목차
- update와 DirtyChecking
- Save의 동작방식 - persist와 merge
- 예제로 알아보자
- merge 후에는 Persistence Context가 관리할까?
- Optional타입으로 find했다면, Persistence Context에는 Optional객체가 관리될까? Optional.get()가 관리될까?
* Persistence Context를 PC로 지칭하겠다.
# Update와 Dirty Checking
JPA는 update를 명시적으로 호출하지 않는다. 보통 DIrty Checking이라는 방식을 사용한다.
변경 감지란 다음과 같이 작동하는 것을 말한다.
- PC에 관리되는 객체에 한해, PC는 초기에 해당 객체의 SnapShot을 만들어 둔다.
- PC에서 관리되고 있는 객체가 Snapshot과 비교해 수정되는 상황이 일어나면, PC는 쓰기 지연 SQL저장소 라는 곳에 UPDATE문을 만들어 보관한다.
- Flush(DB와 PC의 동기화)시점에, Snapshot과 해당 객체의 현재 상태가 다르다면 쓰기 지연 SQL 저장소에 보관해둔 UPDATE문을 날린다.
이와 같은 Dirty Checking이라는 기능덕분에 JPA에서는 굳이 UPDATE문을 날릴 이유가 없다.
하지만, JPA는 merge라는 수정할 수 있는 또 하나의 방법이 존재한다.
merge는 다음과 같이 동작한다.
- 1차 캐시(PC)에서 엔티티를 찾는다.
- 없다면 DB조회 후 1차 캐시에서 관리한다.
- 1번에서 조회된 엔티티를 파라미터로 받은 정보로 병합한다.
하지만 이러한 merge방식은 잘 사용되지 않는다. 왜 그럴까??
- Dirty Checking은 변경한 컬럼만 수정이 가능한 반면, merge는 전체 컬럼을 모두 update한다.
- 파라미터에 null이 실수로 들어온다면 수정할 계획에 없었던 컬럼이 null로 바뀌게된다.
- EntityManager를 직접 호출해야 하는 번거로움이 있다.
Dirty Checking이 원하는 속성만 변경하는 것은 맞지만, 쿼리 보면 모든 컬럼에 대해 Update문이 날아간다.(변경만 되지 않을뿐이다.)
특정 컬럼에 한해서만 Update 쿼리를 날리고 싶다면 @DynamicUpdate 어노테이션을 붙이면 된다.
하지만 성능상 오버헤드가 있다고 하니 가급적 사용하지 않을 것을 권장한다.
이러한 이유로, Merge는 권장되지 않는다. 하지만 자신도 모르는 사이에 merge가 호출되는 경우가 있다.
바로 이 다음에 설명할 Spring Data JPA의 Save() 메소드다.
# Spring Data JPA - Save
기본적으로 JPA의 저장 기능은 바로 DB에 insert하는 것이 아니라 다음과 같이 작동한다.
- persist()를 호출해 PC에서 관리
- flush()시점에 insert를 날림
그렇다면 Spring Data JPA의 Save메소드를 자세히 보자. SimpleJPARepository를 가 보면 볼 수 있다.
보면 Save()는 persist가 되는것이 아니라, 새로운 객체에 한해서만 persist를 호출하고 그렇지 않으면 merge()를 호출한다. 왜 그럴까??
바로 PC가 ID를 이용해 엔티티를 관리하기 때문이다.
새로운 객체라면 망설일 필요가 없이 persist를 하고, flush시점에 insert 쿼리를 DB에 날리면 된다.
하지만 그 객체가 새로운 객체가 아닌 경우는 무엇일까?? 바로 DETACHED상태다
DETACHED(준영속상태)는 이해하기 쉽게 말하면
- 이미 DB에 갔다온 이력이 있는
- = PC에서 관리되었던 이력이 있는
- = 즉 PC에 의해 관리 되었었지만 지금은 아닌 객체
PC가 Id를 이용해 엔티티를 관리하는데, 이미 관리되었던 객체가 다시 PC에 들어왔고, insert쿼리를 날린다
그럼 DB에는 PK가 2개인 상황이 발생하고, 이는 일어나서는 안 될 일이다.
때문에 한번 관리되었던 객체는 DETACHED상태로 관리하는 것이고, Save메소드가 이러한 객체는 insert를 날리는 것이 아닌 merge를 이용해 update쿼리를 날려주는 것이다.
# 예제로 알아보자
함께 프로젝트를 하던 팀원 분의 코드 중 이런 코드를 봤다.
Todo todo = todoRepository.save(new Todo("Hello World"));
em.flush();
em.clear();
Optional<Todo> optionalTodo = todoRepository.findById(todo.getId());
if(optionalTodo.isPresent()) {
optionalTodo.get().setContent("Hello#");
optionalTodo.get().setProgress(Progress.COMPLETED);
todoRepository.save(optionalTodo.get());
}
JPA에 대해 알아보는 것이 목적이므로
- Optional의 null체크 하는 부분이나 객체를 가져오는 부분
- setter에 대한 부분에 대해서는 다루지 않겠다.
DB에 있는 데이터를 대상으로 테스트 하기 위해 일부러 em.flush()를 호출해 동기화 해주었다.
Dirty Checking에 대해 이해했다면, 굳이 setter로 값을 변경했는데 save까지 호출 할 필요가 없다는 것을 알 것이다.
하지만 내가 다루고자 하는 부분은 아래와 같다.
- 두 번째 Save()에서 em.merge()가 실행되는가 em.persist()가 실행되는가??
- em.merge()가 실행된다면, merge()한 객체는 PC의 관리 대상인가??
- findById()에서 그냥 Optional로 객체를 받아버렸는데, 이런 경우 Optional객체도 PC의 관리 대상인가??
위 코드가 잘못된 부분이 있다는 것은 한눈에 알 수 있지만, 2,3번째 궁금증에 대해서는 한번에 확신이 들지 않았다.
# Save()를 두 번 호출했을 때, 어떤 함수가 호출될까??
# em.merge()가 실행된다면, merge()한 객체는 PC의 관리 대상인가??
테스트 코드는 다음과 같다.
//given
Todo todo = todoRepository.save(new Todo("Hello World"));
assertThat(em.contains(todo)).isTrue();
em.flush();
em.clear();
//when
todo.setContent("Hello World2");
Todo updateTodo = todoRepository.save(todo);
//then
assertThat(em.contains(todo)).isFalse();
assertThat(em.contains(updateTodo)).isTrue();
setter와 save()를 동시에 호출한 것이 아니냐? 할 수 있지만
given절에서 flush()와 clear()를 한 번씩 호출 했기 때문에 todo는 더이상 pc의 관리 대상이 아니다.
그러므로 setter를 호출해도 더티체킹 되지 않는다.
결과를 보면 분명 Save()를 호출했음에도 Update쿼리가 날아갔다. Save()호출 시 em.persist()가 된 것이 아니라 em.merge()가 호출되었다는 것을 알 수 있다. 즉, todo는 DETACHED객체였다.
더불어, 검증 코드를 보면 todo는 더이상 pc에서 관리하지 않는다.
또한 merge의 결과인 updateTodo는 pc의 관리 대상이다.
# Optional객체도 PC의 관리 대상인가??
//given
Todo todo = todoRepository.save(new Todo("Hello World"));
em.flush();
em.clear();
//when
Optional<Todo> optionalTodo = todoRepository.findById(todo.getId());
Todo getTodo = optionalTodo.get();
//then
assertThat(em.contains(optionalTodo)).isFalse();
assertThat(em.contains(getTodo)).isTrue();
결과는 그냥 에러다
Persistence Context는 오로지 엔티티만을 관리한다. Optional로 감싸져있는 객체는 애초에 PC의 관리대상이 아니다.
그럼 Optional안의 객체는 PC의 관리 대상일까? 에러가 발생한 밑에서 두 번째 코드를 주석처리하고 다시 실행시켜보자
테스트를 통과했다. PC는 PK로 관리하기 때문에 Optional안의 객체에 대해서는 관리를 해준다.
정리
- update를 위해서는 em.merge()보다 Dirty Checking을 사용하자
- Save는 새로운 객체에 대해서는 persist를, Detach 객체에 대해서는 merge()를 호출한다.
- merge된 객체는 PC의 대상이 되며, Optional객체는 PC가 관리하지 않는다.
- Dirty Checking한 객체에 대해서는 굳이 Save를 호출해주지 않아도 된다.
출처
자바 ORM 표준 JPA 프로그래밍 / 김영한
'JPA' 카테고리의 다른 글
[JPA] Delete와 DeleteById의 차이점에 대해 알아보자 (0) | 2022.06.11 |
---|---|
[JPA] Hibernate 6 : Save() 메소드 Deprecated (0) | 2022.06.11 |