변경 감지와 병합(merge)

상품을 수정하는 ItemController를 다시 보자.

  • 준영속 엔티티 개념 등장! Book 객체!

@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form){//"form"은 updateItemForm에서 넘어올 때 객체 이름?!<form th:object="${form}" method="post"> 혹은 디폴트이름
  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";
}

이처럼 객체 하나를 생성해서 Form 데이터로 set하고, ItemService의 saveItem(book)으로 변경한 객체를 저장하고 있다. ItemService는 단순히 ItemRepository에 위임하는 것이고 실제로는 ItemRepository의 save 함수로 변경 또는 저장이 일어난다!

ItemRepository save 함수 장점 : 새로운 엔티티 저장과 준영속 엔티티 병합을 편리하게 한번에 처리

상품 리포지토리에선 save() 메서드를 유심히 봐야 하는데, 이 메서드 하나로 저장과 수정(병합)을 다 처 리한다. 코드를 보면 식별자 값이 없으면 새로운 엔티티로 판단해서 persist() 로 영속화하고 만약 식별자 값이 있으면 이미 한번 영속화 되었던 엔티티로 판단해서 merge() 로 수정(병합)한다. 결국 여기서의 저장 (save)이라는 의미는 신규 데이터를 저장하는 것뿐만 아니라 변경된 데이터의 저장이라는 의미도 포함한다. 이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 클라이언트의 로직이 단순해진다.

public void save(Item item){
    //JPA에 저장하기 전까지는 id값이 없다.새로 생성하는 객체라는 뜻
    if(item.getId() == null){//처음에는 id라는 게 없기 때문에.
        em.persist(item);
    } else{//이미 DB에 등록된 것이라는 뜻.
        em.merge(item);//ItemService의 updateItem와 동일. 파라미터의 값으로 영속성 엔티티를 조회해서 가져온 것을 다 바꾼다!
        //1.파라미터로 들어간 item은 영속성 컨텍스트으로 변하지 않고, 2.em.merge(item)의 결과가 영속성 컨텍스트에서 관리되는 객체다! (1 != 2) 쓸거면 2를 써야함.
    }
}

일단 여기서 수정을 시도하는 Book 객체는 준영속 엔티티다! 영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다. Book 객체는 이미 DB에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.

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

  1. 변경 감지 기능 사용 객체를 하나 생성해서 값을 셋팅하는 것이 아니라 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법이다!!!트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 실행한다! 트랜잭션 안에서 엔티티를 조회해야 영속상태로 조회 가능하고, 그 영속 엔티티로 더티 체킹(변경 감지)가 일어날 수 있고, 트랜잭션 커밋 가능하다!

    @Transactional
    void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
        Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한 다.
        findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
    }
  2. 병합( merge ) 사용 (❌) : 기존에 있는 엔티티 병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다. 아래의 파라미터인 itemParam은 준영속 상태의 엔티티이다!

    @Transactional
    void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
        Item mergeItem = em.merge(item);
    }

병합 동작 방식

  1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.

  2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다.(병합한다.)

  3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행

주의⭐️⭐️⭐️⭐️⭐️ 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)

실무에서는 한 엔티티에 10개의 필드가 있다고 한다면 수정을 하는 필드는 3~4개인데 값을 넣어주지 않으면 null로 업데이트해버린다! 병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데 이터를 항상 유지해야 한다. 실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.

가장 좋은 해결 방법

엔티티를 변경할 때는 항상 변경 감지를 사용한다!

  • 컨트롤러에서 어설프게 엔티티를 생성❌

  • 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달(파라미터 or dto)

  • 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회, 엔티티의 데이터를 직접 변경

  • 트랜잭션 커밋 시점에 변경 감지가 실행됨

  • setter로 값을 변경하는 것이 아니라 의미있는 메서드를 만들어서 수정하기!!! 즉, 수정이 일어나는 것을 명확하게 알 수 있는 메서드를 만들면 수정해야할 때에 역추적이 가능하다!

ItemController 클래스 아이템 수정 부분 개선

변경 전 :

@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";
      }

Setter를 사용하지 않고 의미 있는 메서드로 값을 변경한 모습

@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
  itemService.updateItem(form.getId(), form.getName(), form.getPrice());
  return "redirect:/items";
}

ItemService 클래스 updateItem 메서드 Id로 영속상태의 엔티티를 가져온다! 값을 셋팅하고 나면 스프링의 @Transactional에 의해 이 트랜잭션이 커밋된다. 커밋하면 JPA는 flush()한다. 그러면 영속성 컨텍스트 중 변경된 것들 다 찾는다! item이라는 엔티티의 값이 변경된 것을 감지해서 커밋될 때 DB에 업데이트한다!

@Transactional
public void updateItem(Long id, String name, int price) {
  Item item = itemRepository.findOne(id);
  item.setName(name);
  item.setPrice(price);
}

Last updated