JPA 변경감지와 병합
준영속 엔티티란?
@PostMapping("items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form, @PathVariable String itemId) {
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";
}- 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 말한다.
- 위의 코드에서
itemService.saveItem(book)에서 수정을 시도하는book객체이다.book객체는 이미 DB 에 한 번 저장되어서 식별자가 존재한다. 따라서 new 를 통해 새로 만들어낸 엔티티도 기존의 식별자를 가지고 있다면 준영속 엔티티로 볼 수 있다.
준영속 엔티티를 수정하는 두 가지 방법
변경감지 기능 사용 ( dirty checking ) - best practice
@Transactional void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티 Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다. 이때 조회함으로해서 해당 엔티티 객체는 persist 상태가 된다. findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다. 객체는 persist 상태이기 때문에 save 메소드를 호출하지 않아도 변경이 감지되어 트랜잭션 커밋 시점에 업데이트 쿼리가 날아간다. }병합 사용 ( merge )

@Transactional void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티 Item mergeItem = em.merge(item); }병합의 동작 방식
- merge() 를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
- 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀐다.)
- 영속 상태인 mergeMember를 반환한다.
변경 감지가 병합에 비해 best - practice 인 이유?
- 변경 감지 기능을 사용하면 언하는 속섬안 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 따라서 병합시 값이 없다면
null로 업데이트 될 위험이 존재한다. ( 병합은 모든 필드를 교체하므로 ) - 실무에서는 보통 업데이트 기능이 매우 재한적이다. 그런데 병합은 모든 필드를 변경해버리고, 데이터가 없으면
null로 업데이트 해버린다. 병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데이터를 항상 유지해야 한다. 실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
가장 좋은 해결 방법 - 엔티티를 변경할 때에는 항상 변경 감지를 사용하자 !
- 컨트롤러에서 어설프게 엔티티를 생성해서는 안된다.
- 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달해야 한다. ( 파라미터 or DTO )
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경해야 한다.
- 따라서 트랜잭션이 커밋되는 시점에 변경 감지가 실행된다.
/**
* 상품 수정, 권장 코드
*/
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
itemService.updateItem(form.getId(), form.getName(), form.getPrice());
return "redirect:/items";
}
}
/**
* 영속성 컨텍스트가 자동 변경
*/
@Transactional
public void updateItem(Long id, String name, int price) {
Item item = itemRepository.findOne(id);
// item.setName(name); // setter 도 사용 비추. setter 대신 도메인 단에 명시적인 메소드를 만들어서 사용하자.
// item.setPrice(price);
item.change(name, price) // 추적 가능성을 높이기 위한 도메인 단 엔티티 변경 메소드
}
데이터를 수정할 때에는 Spring Data JPA 의 save() 메소드를 함부로 사용하지 않아야겠다 !
REFERENCES
'Spring Data' 카테고리의 다른 글
| Querydsl 초급 문법 (0) | 2021.11.04 |
|---|---|
| JPA Open Session In View (0) | 2021.11.03 |
| JPA Proxy (0) | 2021.11.03 |
| 연관관계 주인과 mappedBy (0) | 2021.11.03 |
| @OneToOne, @ManyToMany (0) | 2021.11.03 |