준영속 엔티티란?

  • 영속성 컨텍스트엔티티를 영구 저장하는 환경이다. 엔티티가 이 컨텍스트 안에 있으면 영속 상태, 밖으로 나오면 준영속 상태가 된다.
  • 트랜잭션 안에서 엔티티의 값이 변경되면 JPA에서 이를 감지하고, COMMIT 시점에 해당 변경사항을 DB에 자동으로 반영해준다.
  • JPA 영속성 컨텍스트가 위처럼 더 이상 관리하지 않는 엔티티(Primary Key 등의 식별자 값이 이미 존재하는 것 처럼 DB에 이미 한 번 저장된 적이 있는 데이터)를 준영속 엔티티라고 한다.
  • 임의로 만든 엔티티라도, 식별자 값이 있으면 (엔티티 객체를 만들고 해당 엔티티에 식별자 값을 대입한 경우 등) 이 또한 준영속 엔티티에 해당된다.
@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());
    ...
    
    itemService.saveItem(book);
    return "redirect:/items";
}
  • 위 같은 경우, 임으로 만든 book 엔티티는 JPA에서 자동으로 COMMIT 시점에 변경사항을 DB에 반영해주지 않는다.
  • new 로 만들고 id만 넣으면 → 영속성 컨텍스트에 등록된 적이 없으므로 준영속 엔티티이다.
  • 즉, 준영속 엔티티는 JPA에서 자동으로 변경 사항을 추적하고 반영해줄 근거가 없는 것이다.
  • 따라서 이러한 준영속 엔티티를 수정하는 방법에는 변경 감지(dirty checking)와 병합(merge)의 두 가지 방법이 있다.

 

변경 감지(dirty checking)

@Transactional
public void updateItem(Long itemId, String bookName, int price, int stockQuantity) {
    Item findItem = itemRepository.findOne(itemId);

    findItem.setPrice(price);
    findItem.setName(bookName);
    findItem.setStockQuantity(stockQuantity);
}
  • 위 방식은 준영속 엔티티의 id를 이용해 영속 엔티티를 다시 조회한 후 그 영속 엔티티를 수정하여 변경 감지 기능을 사용하는 것이다. itemId를 통해 DB에 저장되어 있는 영속 상태의 엔티티를 가지고 와서 수정한다.
  • em.find(), JPARepository.findById()로 가져오면 → 조회 순간 영속성 컨텍스트에 등록되므로 영속 엔티티이다.
  • 그러면 영속 엔티티이기 때문에, JPA에서 변경 사항을 감지해서 COMMIT 시점에 자동으로 UPDATE SQL을 실행해준다.
  • 여기서는 서비스 계층에서 일일이 직접 set을 해주었지만, 이렇게 하지 않고 엔티티에 change(price, name...) 처럼 의미있는 명확한 메서드를 만들어서 사용함으로써 변경이 어디에서 이루어지는 추적할 수 있도록 하자.
  • 즉, 저렇게 일일이 직접 set 사용은 지양하자. 의미가 명확하지 않다. 엔티티 안에서 바로 추적할 수 있는 함수를 만들자.

 

병합(merge)

  • 준영속 상태의 엔티티를 영속 상태로 바꾼다.

병합 동작 방식

 

// ItemService
@Transactional
public void save(Item item) {
    itemRepository.save(item);
}


// ItemRepository
public void save(Item item) {
    // 저장하기 전에는 ID가 없으므로 새롭게 저장
    if (item.getId() == null) {
        em.persist(item);
    } else {
        em.merge(item);
    }
}
  • 변경 감지에서 했던 것처럼, item에 set을 해서 값을 다 채워주고 값이 채워진 엔티티를 반환하는 기능을 merge 코드 한 줄이 자동으로 해준다.
  • 하지만 merge 함수 파라미터로 전달된 item은 준영속 엔티티이고, merge 함수가 반환해주는 item이 영속 엔티티이다.
  • 변경 감지 방식은 필요한 속성만 선택해서 변경할 수 있지만, 병합 방식은 해당 엔티티의 모든 속성이 set 됨으로써 변경된다. 즉, 파라미터로 넘어온 엔티티 속성에 null 값이 있으면 null로 채워질 수도 있는 것이다.

 

 

즉, 병합 방식 사용은 지양하고 변경 감지 기능을 사용해서 준영속 엔티티를 변경하자.

 

 

 

컨트롤러에서 엔티티 생성을 지양하자

  • 말 그대로 컨트롤러에서 입력으로 받은 파라미터를 통해 엔티티를 생성하는 것을 지양하자. 트랜잭션이 있는 서비스 계층에서 식별자와 변경할 데이터를 명확하게 전달해야 하기 때문이다.
  • 즉, 컨트롤러에서 엔티티 생성 -> 서비스 계층에 엔티티 넘기는 방식은 지양이다.
  • 컨트롤러에서 DTO로 데이터 받고 DTO를 서비스 계층에 넘기기 -> 서비스 계층에서 엔티티의 팩토리 메서드로 엔티티 생성. 이런식으로 하자.
  • 그래야 트랜잭션이 있는 서비스 계층에서 엔티티를 영속 상태로 조회할 수 있고, JPA가 변경 사항을 추적할 수 있기 때문이다.
// 컨트롤러 -> DTO로만 받기
@PostMapping("/items/{itemId}/edit")
public String update(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
    itemService.updateBook(itemId, form);
    return "redirect:/items";
}


// 트랜잭션이 있는 서비스 계층에서 영속 엔티티 조회 + 변경 감지 사용
@Service
@Transactional
public class ItemService {

    private final ItemRepository itemRepository;

    public void updateBook(Long itemId, BookForm form) {
        Book book = itemRepository.findOne(itemId); // 영속 엔티티

        // 영속 엔티티 수정 → dirty checking
        book.change(form.getName(), form.getPrice(), form.getStockQuantity());
    }
}


// 엔티티 내부에는 의미 있는 메서드를 제공
@Entity
public class Book extends Item {

    public void change(String name, int price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
}