![article thumbnail image](https://blog.kakaocdn.net/dn/cWZ4Us/btrFbN0H9F0/k85xDkqSOjlKzRvtKbIvy1/img.png)
변경 감지(Dirty Checking)
JPA에서 사용하는 기본 메커니즘으로 실제 데이터베이스와 값과 엔티티의 값이 변화된 부분을 감지할 때 사용하는 패턴이다.
공부를 하고 정리를 하다보니 생각난 부분은 더티 체킹과 유사한 기법을 생각보다 많은 사람이 경험하고 있지 않나라는 생각이였다.
그것은 바로 필자처럼 흔히들 Javascript로 개발을 시작했다면 쉽게 접해볼 Vue.js와 React.js에서도 유사한 개념이 나온다.
Vue.js나 React.js의 경우 가상돔(Virtual Dom)
이란 개념을 이용해서 화면에서 변경된 부분만을 리렌더링하며 효율적으로 관리한다.
해당 부분에서 더티 체킹 기법이 구현된 방법 중 하나로 사용된다.
준영속 엔티티
준영속 엔티티는 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 말한다.
준영속 엔티티가 된 경우에는 더 이상 JPA가 관리를 해주지 않기 때문에 더티 체킹의 대상에 포함되지 않는다.
준영속으로 만드는 방법
em.detach(entity)
detach
는 사전적으로 분리하다라는 의미다.
사전적 의미와 유사하게 더이상 JPA에게 해당 엔티티를 관리하지 말라는 메소드다. 메소드를 호출하면 1차 캐시와 지연 쓰기 SQL 저장소까지 해당 엔티티에 대한 모든 정보가 제거된다.
em.clear()
영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
detach
와 다른 점은 detach
의 경우는 해당 엔티티의 정보만 제거하고 clear는 컨텍스트의 모든 정보를 제거한다.
em.close()
영속성 컨텍스트를 종료하는 메소드로 해당 영속성 컨텍스트가 관리하던 모든 정보가 준영속 상태가 된다.
detach
, clear
와 다른 점은 정보를 제거하지 않고 준영속 상태로만 만든다.
준영속 엔티티를 수정하는 방법
준영속 엔티티를 수정하는 방법은 두 가지가 있다.
변경 감지(Dirty Checking)
을 이용하는 방법병합(Merge)
를 이용하는 방법
문제의 코드
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice()); book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
book객체는 새로 생성한 후에 form객체를 통해서 값들을 넣어주며 JPA가 식별 가능한 ID값을 가지고 있지만 이미 DB를 거친 준영속 엔티티다.
준영속 엔티티여서 아무리 데이터를 바꿔도 실제 데이터베이스에는 반영되지 않는다.
해당 코드에서는 saveItem을 통해서 명시적으로 persist를 해서 데이터베이스에 반영했다.
변경 감지(Dirty Checking)을 이용하는 방법
@Transactional
public void updateItem(Long itemId, Book book){
// 영속성 엔티티를 찾아옴
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(book.getPrice());
findItem.setName(book.getName());
findItem.setStockQuantity(book.getStockQuantity());
/**
* 이 이후에 Transaction이 커밋되면서 더티 체킹을 통해서 바뀐 값이 반영된다.
*/
}
더티 체킹을 이용하는 방법은 영속성 컨텍스트가 관리해주는 엔티티를 같은 트랜잭션 내에서 데이터를 바꿔주면 된다.
현재 코드의 경우 itemRepository
에서 findOne
메소드를 통해서 새로 아이템을 찾아온다.
이 시점에서 영속성 엔티티를 찾아오게 되었고 Setter를 이용해 값만 넣어 주면 트랜잭션이 커밋되는 시점에 반영이된다.
Setter의 경우 최대한 제거해주는 것이 좋기 때문에 해당 코드를 개선하면 다음과 같이 된다.
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity){
// 영속성 엔티티를 찾아옴
Item findItem = itemRepository.findOne(itemId);
findItem.change(name, price, stockQuantity);
}
Item Entity
public void change(String name, int price, int stockQuantity) {
this.price = price;
this.name = name;
this.stockQuantity = stockQuantity;
}
병합(Merge)를 이용하는 방법
다음은 병합을 이용하는 방법이다.
병합은 준영속 엔티티를 영속 엔티티로 변경할 때 사용하는 기능이다.
다음 그림의 플로우는 다음과 같다.
- merge(member)가 실행된다.
- 파라미터로 전달된 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회
- 만약 1차 캐시에 없다면 데이터베이스에서 조회하고 1차 캐시에 저장
- 조회한 영속 엔티티에 현재 가지고 있는 준영속 엔티티의 값으로 모두 교체
- 영속 엔티티인 mergeMember를 리턴한다.
em.merge(entity)를 코드로 옮기면 다음과 같이 된다.
@Transactional
public Item updateItem(Long itemId, Book book){
// 영속성 엔티티를 찾아옴
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(book.getPrice());
findItem.setName(book.getName());
findItem.setStockQuantity(book.getStockQuantity());
return findItem;
}
영속 엔티티를 찾고 가지고 있던 준영속 엔티티의 값으로 모두 교체해서 리턴한다.
하지만 여기서 병합을 사용하지 말아야 할 이유가 있다.
더티 체킹을 이용해서 값을 변경하는 경우에는 직접 로직을 통해서 다루며 원하는 값만 바꿀 수 있다.
병합의 경우에는 엔티티를 전달하면서 내부 로직이 개발자가 제어하는 영역에서 이뤄지지 않는데 병합은 모든 필드의 값을 모두 바꾼다.
말만 들어보면 병합이 무엇이 문제가 되는지 잘 감이 오지 않는다.
해당 방식이 문제가 되는 이유를 예를 들어 살펴보자
다음과 같은 Item 클래스가 있고 Item 클래스는 최초 생성된 이후에 가격을 변경할 수 없다는 정책이 있다고 해보자
public class Item {
private Long id;
private String name;
private int price;
private int stockQuantity;
}
그런 경우 업데이트를 할 경우에는 재고량과 이름만 바꿀 수 있으니 파라미터에는 name, stockQuantity의 값만 전달될 것이다.
여기서 병합을 이용해 값을 바꾸는 방법을 사용한다면 다음과 같이 된다.
@Transactional
void update(Item item) {
Item mergeItem = em.merge(item);
}
위 코드의 em.merge가 실행되는 시점에 item객체의 내부는 아래와 같이 price가 null인 상태가 되고 실제 데이터베이스에는 정책에 맞지 않게 price의 값까지 null로 반영되버린다.
{
name: "test",
price: null,
stockQuantity: 10
}
이러한 문제점 때문에 최대한 병합(Merge)
은 사용하지 않고 변경 감지(Dirty Checking)
을 이용해 비즈니스 로직을 구현해야 한다.
'SpringBoot' 카테고리의 다른 글
[JPA 활용] 지연 로딩과 조회 성능 최적화 - 엔티티 직접 노출하기 (0) | 2022.07.05 |
---|---|
Querydsl 최초 설정하기 (0) | 2022.06.27 |
HTTP Status Code 제어 및 Exception Handling (0) | 2022.04.04 |
User Service API 구현 (0) | 2022.03.22 |