이번에는 변경감지와 병합에 대해서 알아보려고 합니다.
변경 감지와 병합을 알기 전에는 영속성 컨텍스트를 먼저 알아야 하는데요.
영속성 컨텍스트
영속성 컨텍스트는 엔티티를 영구 저장하는 공간입니다. 엔티티 매니저로 엔티티를 저장하거나 데이터베이스에서
조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리합니다.
엔티티 저장
엔티티를 등록할 때 persist() 메서드를 실행하면 바로 데이터베이스에 저장하는게 아닌 영속성 컨텍스트의
내부 쿼리 저장소에 INSERT SQL 문을 쌓아 먼저 저장하게 되며, 트랜잭션 커밋하면 영속성 컨텍스트의 변경 내용을
데이터베이스에 동기화하는 작업인 플러시를 실행해 모아둔 쿼리를 데이터베이스에 반영합니다. JPQL 을 실행하거나
flush() 메서드를 이용해 플러시를 직접 실행해도 영속성 컨텍스트에 모아둔 쿼리들을 한꺼번에 실행해 데이터베이스에
반영합니다.
̱ 엔티티 조회
find() 메서드로 엔티티를 조회할 떄는 먼저 영속성 컨텍스트에서 해당 엔티티를 찾고 만약 찾는 엔티티가 없으면
데이터베이스에서 조회합니다. 데이터베이스에서 조회하면 엔티티를 생성한 후, 영속성 컨텍스트에 저장하고
영속 상태의 엔티티를 반환합니다. 여기서 영속이란 영속성 컨텍스트에 저장된 상태를 의미합니다.
엔티티 삭제
remove() 메서드로 엔티티를 삭제할 때도 엔티티를 즉시 삭제하는게 아닌 삭제 쿼리를 내부 쿼리 저장소에 먼저
등록하고, 트랜잭션을 커밋하면 실제 데이터베이스에 삭제 쿼리를 전달합니다.
엔티티를 삭제할 때는 먼저 엔티티를 조회한 후 삭제를 하는데요. 데이터베이스에서 엔티티를 조회하면서 엔티티가
영속성 컨텍스트를 거치며 등록되고 이후 remove() 메서드를 호출해 해당 엔티티를 지정해 삭제하면 그 엔티티는
영속성 컨텍스트에서 제거됩니다.
엔티티 수정
엔티티 수정의 경우 SQL 문을 사용할 때는 직접 수정 쿼리를 작성해야 하지만 JPA 에서는 다릅니다. 엔티티가
영속 상태일 때 값만 바꾸면 JPA 가 트랜잭션 커밋 시점에 변경된 내용을 알고 데이터베이스에에 반영합니다.
이것을 변경 감지라고 합니다. find() 메서드로 엔티티를 찾아오면 영속성 컨텍스트에도 엔티티 존재하게 되고
이때 수정자 setXX() 메서드로 값을 바꾸면 변경 쿼리가 내부 쿼리 저장소에 등록되고 트랜잭션 커밋하거나
플러시를 호출하면 변경 쿼리가 데이터베이스에 반영됩니다.
이 변경 감지 기능은 어떻게 동작할 수 있을까요?
JPA 는 엔티티를 영속성 컨텍스트에 보관할 떄, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 합니다.
그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾아 변경된 엔티티가 있으면 수정 쿼리를
생성해 내부 쿼리 저장소에 보내게 됩니다.
변경 감지는 영속성 컨텍스트에서 관리하는 영속 상태의 엔티티에만 적용되는데요. 영속성 컨텍스트에서 관리되었다
분리되어 영속성 컨텍스트에서 더 이상 관리하지 않는 엔티티를 준영속 엔티티라고 합니다.
엔티티를 준영속 엔티티로 만들려면 detach(entity) 메서드로 특정 엔티티만 준영속 상태로 전환,
close() 메서드를 호출해 영속성 컨텍스트를 닫기,
clear() 메서드를 호출해서 영속성 컨텍스트를 초기화하면 영속성 컨텍스트가 관리하는 영속 상태의 엔티티는
준영속 상태가 됩니다. 또한 임의로 만들어낸 엔티티도 기존 식별자 가지고 있으면 준영속 엔티티로 볼 수 있습니다.
만약 준영속 엔티티의 값이 바뀐다면 어떻게 될까요?
이 때는 변경 감지가 동작하지 않기 때문에 merge() 메서드를 호출해 병합해야 합니다. 병합은 merge() 메서드
인자값으로 들어온 엔티티의 식별자에 해당하는 엔티티를 영속성 컨텍스트에서 찾고 없다면, find() 메서드를 통해
엔티티를 데이터베이스에서 조회한 후 조회한 엔티티의 모든 값을 인자값으로 들어온 엔티티의 값으로 변경해줍니다.
이렇게 하면 값이 변경된 엔티티는 영속성 컨텍스트에 존재하니 값을 변경하면 변경 감지가 동작해 수정 쿼리가
내부 쿼리 저장소에 저장되고 트랜잭션을 커밋하면 실제 데이터베이스에 수정 쿼리가 반영됩니댜.
merge() 의 반환값은 값을 변경한 새로 가져온 엔티티이며 인자값으로 들어온 엔티티 객체가 영속 상태로 바뀌는건
아닙니다.
주의점
변경 감지는 수정자인 setXX() 메서드로 지정한 필드만 변경되는데 병합은 모든 필드가 변경됩니다.
병합시 인자값으로 들어온 엔티티 필드 중 값이 없는 필드는 null 로 변경됩니다. 이 경우 데이터베이스에도 그 필드에
해당하는 컬럼은 null 로 변경되게 됩니다. 이런 문제가 발생하지 않게 하려면 변경하려는 엔티티의 식별자를 이용해
데이터베이스에서 엔티티를 조회한 후, 정말 변경이 필요한 필드만 변경하면 됩니다.
에제로 더 자세히 알아보곘습니다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
Book item = (Book) itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
model.addAttribute("form", form);
return "items/updateItemForm";
}
@PostMapping("/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
}
GET 메서드로 /items/{itemId}/edit url 요청을 하면 url 에 있는 상품 식별자로 상품 엔티티를 가져오고
상품 엔티티의 값들을 뷰에 보여줄 DTO(Data Transfer Object) 인 BookForm 클래스 객체에 넣어줍니다.
화면에서 값을 수정하고 POST 메서드로 /items/{itemId}/edit url 요청을 하면 상품의 식별자와 변경하려는 값을
서비스 메서드 인자값으로 주어 서비스 계층에 보냅니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId, String name, int price, int stackQuantity) {
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(price);
findItem.setName(name);
findItem.setStockQuantity(stackQuantity);
}
}
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public Item findOne(Long id) {
return em.find(Item.class, id);
}
}
서비스 계층에서 인자값으로 받은 상품 식별자로 엔티티를 조회합니다. 그리고 가져온 엔티티 객체를
수정자를 통해 필요한 필드만 변경합니다. 현재 ItemService 클래스의 updateItem() 메서드는 @Transactional
어노테이션을 적용했기 때문에 메서드 안의 코드가 실행중 에러가 나지 않으면 커밋이 수행됩니다.
조회된 엔티티는 영속성 컨텍스트에서 관리되며, 영속성 컨텍스트에서 관리되는 엔티티의 값이 변경되었으므로
수정 쿼리가 내부 쿼리 저장소에 쌓여있다가 커밋이 수행될 때 데이터베이스에 반영됩니다.
이 포스팅은 자바 ORM 표준 JPA 프로그래밍의 내용을 참고하여 작성하였습니다.
'JPA' 카테고리의 다른 글
[JPA] JPQL(객체 지향 쿼리 언어) (0) | 2022.11.15 |
---|---|
[JPA] 프록시, 즉시 로딩, 지연 로딩, 영속성 전이, 고아 객체 (0) | 2022.11.13 |
[JPA] 다양한 연관관계 매핑 (0) | 2022.10.31 |
[JPA] 연관관계 매핑 (0) | 2022.10.24 |
[JPA] 상속 관계 매핑 (0) | 2022.10.08 |
댓글