• spring
  • jpa
  • java

JPA 변경 감지(dirty checking)와 병합(merge)

준영속 엔티티를 수정하는 두 가지 방법인 병합과 변경 감지의 차이, 그리고 변경 감지를 써야 하는 이유를 정리한다.

시리즈 · Spring12 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

영속성 컨텍스트와 준영속

영속성 컨텍스트(persistence context)

  • 엔티티 매니저로 엔티티를 저장하거나 조회하면 영속성 컨텍스트가 엔티티를 보관하고 관리한다.
  • 트랜잭션을 커밋할 때 영속성 컨텍스트에서 새로 저장되거나 수정된 엔티티를 DB에 반영한다(flush).
  • 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.

준영속(detached)

  • 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 뜻한다.

수정 폼에서 넘어온 데이터로 엔티티를 갱신할 때, 변경 내용을 DB에 반영하는 방법은 두 가지다.

병합(merge)

@PostMapping("items/{itemId}/edit")
public String updateItem(@PathVariable String itemId, @ModelAttribute("form") BookForm form) {

    Book book = new Book();
    book.setId(form.getId());
    // ... 나머지 값 설정

    itemService.saveItem(book);
    return "redirect:/items";
}
public void saveItem(Item item) {
    if (item.getId() == null) {
        em.persist(item); // 신규 등록
    } else {
        em.merge(item); // 병합
    }
}

폼 데이터를 새로 만든 Book 객체에 setter로 채워 넣어도 DB에는 자동으로 반영되지 않는다. 이 book은 직접 new로 만든 객체일 뿐, 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 아니기 때문이다. 식별자는 갖고 있지만 관리되지 않는 준영속 상태다.

이를 반영하는 방법이 merge다. 같은 식별자를 가진 영속 엔티티에 내가 만든 book의 값을 병합해 준다.

문제는 병합이 모든 필드를 교체한다는 점이다. 값을 설정하지 않은 필드도 그대로 병합되므로 기존 값이 null로 덮어써질 수 있다. 그래서 실무에서는 병합을 사용하지 않는 것이 좋다.

변경 감지(dirty checking)

@Transactional
void update(Item itemParam) { // itemParam: 파라미터로 넘어온 준영속 상태의 엔티티
    Item findItem = em.find(Item.class, itemParam.getId()); // 같은 식별자의 엔티티를 조회한다.
    findItem.setPrice(itemParam.getPrice()); // 데이터를 수정한다.
}

여기서 findItem도 merge를 호출해야 할까? 그럴 필요가 없다. em.find로 조회한 엔티티는 영속 상태이기 때문이다. 트랜잭션이 커밋될 때 영속성 컨텍스트가 변경된 부분을 감지해 UPDATE를 자동으로 반영한다.

변경 감지는 원하는 필드만 골라서 수정할 수 있으므로 병합처럼 의도하지 않은 null 덮어쓰기가 생기지 않는다.

참고