JPA 활용편 - 웹 계층 개발 2 (변경감지와 병합)

Stella·2022년 5월 13일
0

Java

목록 보기
13/18

변경감지(Dirty Checking)와 병합(Merge)

준영속 Entity?

  • 영속성 컨텍스트가 더는 관리하지 않는 entity.
  • 예를 들어, 아래 코드에서 itemService.saveItem(book)에서 수정을 시도하는 Book 객체. Book 객체는 이미 DB에 한번 저장되어서 식별자가 존재. DB에 한 번 다녀온 것 들. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있음.
  • 강제로 영속화를 취소한 엔티티.
  • JPA가 관리를 안해서 변경감지가 일어나지 않음.
  • JPA가 관리하는 영속성 컨텍스트 같은 경우는 변경감지가 일어남.그래서 commit시점에 변경되어있으면 변경.
 @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") String itemId, @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";

    }

준영속 entity를 수정하는 2가지 방법

  1. 변경 감지 기능 사용.
  2. 병합(Merge) 사용.

변경 감지 기능 사용

    @Transactional
    public void updateItem(Long itemId, Book param){//param: 파라미터로 넘어온 준영속 상태의 엔티티
        Item findItem = itemRepository.findOne(itemId);
        findItem.setPrice(param.getPrice());
        findItem.setName(param.getName());
        findItem.setStockQuantity(param.getStockQuantity());
    }
  • 트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 -> 트랜잭션 커밋 시전에 변경감지(Dirty Checking)이 동작해서 데이터베이스에 update sql 실행.
  • findItem은 영속상태. repository에서 찾아옴. spring의 Transactional에 의해서 transaction이 commit이 됨.
  • commit이 되면 jpa가 flush를 날려서 영속성 컨테스트의 변경을 감지.
  • 변경사항을 update 쿼리를 날려서 update 함.

병합(Merge) 사용

  • Merge: 준영속상태의 엔티티를 영속상태로 변경할 때 사용하는 기능.
@Transactional
void update(Item itemParam) { //itemParam: 파라미터로 넘어온 준영속 상태의 엔티티
	Item mergeItem = em.merge(item);
}

병합 동작방식

  1. Merge() 실행.
  2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회.
    2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장.
  3. 조회한 영속 엔티티에 준영속 엔티티의 값을 채워넣음.
  4. 영속 상태인 엔티티를 반환.

병합시 동작 방식 간단 정리

  1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회.
  2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체(병합).
  3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에서 Update sql이 실행.

주의!!! 변경감지 기능을 사용하면 원하는 속성만 변경할 수 있지만, 병합을 사용하면 모든 속성이 변견됨. 병합시 값이 없으면 null로 변경 될 위험 있음.(실무에서는 가급적으로 merge사용하지 않음)

가장 좋은 해결 방법

엔티티를 변경할 때는 항상 변경 감지를 사용하기

  • 컨트롤러에서 어설프게 엔티티를 생성하지말기.
  • 트랜잭션이 있는 서비스 계층에 id와 변경할 데이터를 명확하게 전달.(파라미터 or dto)
  • 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경.
  • 트랜잭션 커밋 시점에 변경 감지가 실행.

ItemController

@Controller
@RequiredArgsConstructor
public class ItemController {
 private final ItemService itemService;
 /**
 * 상품 수정, 권장 코드
 */
 @PostMapping(value = "/items/{itemId}/edit")
 public String updateItem(@ModelAttribute("form") BookForm form) {
 itemService.updateItem(form.getId(), form.getName(), form.getPrice());
 return "redirect:/items";
 }
}

ItemService

package jpabook.jpashop.service;
@Service
@RequiredArgsConstructor
public class ItemService {
 private final ItemRepository itemRepository;
 /**
 * 영속성 컨텍스트가 자동 변경
 */
 @Transactional
 public void updateItem(Long id, String name, int price) {
 Item item = itemRepository.findOne(id); // id가 있으면 transactional 안에서 entity를 조회해야 영속성 컨테스트가 됨. 거기에 값을 변경해야 dirty checking 가능.
 item.setName(name);
 item.setPrice(price);
 }
}

상품 리포지토리의 저장 메서드 분석

ItemRepository

@Repository
public class ItemRepository {
 	
    @PersistenceContext
 	EntityManager em;
 	
    public void save(Item item) {
 		if (item.getId() == null) {
 			em.persist(item);
 		} else {
 			em.merge(item);
 		}
 }
  • save() 는 id 값이 없으면(null) 새로운 엔티티로 판단해서 영속화(persist), id가 있으면 병합(merge).
  • 지금처럼 준영속 상태인 상품 엔티티를 수정할 때는 id 값이 있으므로 병합 수행.
  • 새로운 엔티티 저장과 준영속 엔티티 병합을 편리하게 하나의 method에서 처리 가능.
  • save()는 id를 자동생성해야 정상작동됨. Item entity는 @GeneratedValue 선언해서 사용 중 -> persist()하면서 id 자동생성.
  • id를 직접 할당하도록 설정 된경우 -> id가 없는 상태로 persist() -> error(id 없음).

변경 시 의미있는 method 만들어서 사용

  • Setter 사용 지양.
  • 의미 있는 method를 만들어서 사용 -> 변경지점이 다 entity에서 발생.
@Transactional
    public void updateItem(Long itemId, Book param){ 
        Item findItem = itemRepository.findOne(itemId);
        
        // 이렇게 의미있는 method 만들기
        findItem.change(price,name,stockQuantity);
        findItem.addStock(10);
        
        // setter 사용 지양
        findItem.setPrice(param.getPrice());
        findItem.setName(param.getName());
        findItem.setStockQuantity(param.getStockQuantity());
      return findItem;
    }
profile
Hello!

0개의 댓글