준영속 엔티티란? 영속석 컨텍스트가 더는 관리하지 않는 엔티티를 말한다. 준영속 엔티티를 수정하는 방법으로는 변경 감지 또는 병합(merge)을 사용하는 방법이 있다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@PostMapping("/items/{itemId}/edit")
public String updateItem(@ModelAttribute BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setAuthor(form.getAuthor());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setIsbn(form.getIsbn());
book.setStockQuantity(form.getStockQuantity());
itemService.updateItem(book.getId(), book);
// itemService.saveItem(book); 병합을 통합 update
return "redirect:/items";
}
}
book
엔티티는 준영속 엔티티이다. 즉 임의로 만들어낸 엔티티이더라도 기존 식별자를 가지고 있으면 준영속 상태이다. (이때 식별자는 영속성 컨텍스트에서 엔티티 간 식별을 위해 관리된 식별자이다.)@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemService {
private final ItemRepository itemRepository;
// 병합을 통한 update
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
// 변경감지를 통한 update
@Transactional
public void updateItem(Long itemId, Book bookParam) {
Book findItem = (Book)itemRepository.findOne(itemId); // 이때의 book은 영속상태이므로 변경감지로 수정이 가능하다.
findItem.setName(bookParam.getName());
findItem.setPrice(bookParam.getPrice());
findItem.setAuthor(bookParam.getAuthor());
findItem.setIsbn(bookParam.getIsbn());
findItem.setStockQuantity(bookParam.getStockQuantity());
}
}
병합을 사용하는 경우, 준영속 엔티티가 인자로 전달되면서 saveItem()
메소드가 호출된다. 그리고 ItemRepository에서 준영속 엔티티를 대상으로 merge가 실행된다.
변경감지를 사용하는 경우,
@Transactional
이 붙어 있는 서비스 계층에서 엔티티 변경을 시도한다.
itemRepository.findOne(itemId)
메소드 호출을 통해 db에서 엔티티를 가져온다. 그리고 이때 찾아온 엔티티는 영속성 컨텍스트에 보관되고 따라서 findItem
은 영속 상태이다.
수정할 데이터(bookParam
)를 통해 영속 상태인 findItem
을 수정한다.
트랜잭션이 커밋되는 시점, 하나의 트랜잭션은 하나의 영속성 컨텍스트를 보관하므로, 수정된 내용이 변경감지를 통해 db에 정상 반영된다.
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if(item.getId() == null) { // 새로 생성된 객체
em.persist(item);
} else {
em.merge(item); // 병합을 통한 update
}
}
}
준영속 상태의 엔티티를 대상으로 merge()
를 실행한다.
파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티(mergeMember
)를 조회한다. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장해둔다.
조회한 영속 엔티티(mergeMember
)에 member
엔티티의 값을 채워 넣는다.
영속 상태인 mergeMember
를 반환한다.
트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 update sql이 실행된다.
병합의 동작 방식은 위와 같다. 따라서 merge 후에 이어서 엔티티 객체를 활용할 경우, 준영속 상태인 member
가 아닌, 영속 상태인 mergeMember
객체를 사용해야 한다.
엔티티의 업데이트되는 영역은 제한적이다. 그런데 병합은 엔티티의 모든 필드를 변경해버린다. 만약 가격은 업데이트하지 않겠다는 의도로 업데이트 폼에 price
필드 값이 없는 경우, 병합을 사용하면 price
필드를 null
로 업데이트 해버린다. 즉 이런 문제를 예방하려면 업데이트 폼 화면에서 엔티티의 모든 필드 데이터를 입력받아야 한다. 그러나 실무에서는 보통 변경가능한 데이터만 노출하기 때문에 병합을 사용하는 것이 오히려 번거롭다.
따라서 엔티티를 변경할 때는 항상 변경 감지를 사용하는 것이 좋다.
준영속 엔티티 수정에 대해 아주 상세하게 설명해 주셨네요. 특히 병합보다 변경 감지를 사용하는 것이 더 효과적이라는 점, 잘 알았습니다. 좋은 정보 공유해주셔서 감사합니다!