JPA를 사용하다 보면 "준영속 상태"라는 개념을 접하게된다.
처음엔 어렵게 느껴지지만, 실무에서 엔티티를 수정할 때 꼭 이해하고 넘어가야 할 개념이다.
이번 글에서는 준영속 상태가 무엇인지, 왜 중요한지, 그리고 실무에서는 어떻게 다뤄야 하는지 알아보자.
엔티티란?
- JPA 에서 Entity 는 데이터베이스 테이블에 매핑되는 자바 클래스를 의미한다.
- @Entity, @Id 애노테이션을 통해 정의하며, 하나의 객체 인스턴스는 테이블의 한 row를 의미한다.
@Entity
public class Book {
@Id
private Long id;
private String name;
private int price;
// 기타 필드 및 getter/setter 생략
}
특징
엔티티는 EnitityManager 에 의해 관리되어야 DB 에 저장, 수정, 삭제가 가능하다.
JPA 의 엔티티 생명주기
JPA의 엔티티는 다음과 같은 생명주기를 가진다
- 비영속 (new): 아직 영속성 컨텍스트에 저장되지 않은 상태
- 영속: EntityManager를 통해 관리되고 있는 상태
- 준영속 (detached): 한 번 저장된 적이 있지만, 현재는 EntityManager가 관리하지 않는 상태
- 삭제: 삭제 명령이 수행된 상태
준영속 상태란?
이미 DB 에 저장된 적이 있는 객체인데, 현재 영속성 컨텍스트가 관리하고 있지 않은 상태
아래 코드와 같은 Book 객체도 준영속이라고 정의할까?
@PostMapping("items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form, @PathVariable String itemId) {
Book book = new Book();
book.setIsbn(form.getIsbn());
book.setAuthor(form.getAuthor());
book.setStockQuantity(form.getStockQuantity());
book.setPrice(form.getPrice());
book.setName(form.getName());
book.setId(form.getId()); // DB에 있던 ID
itemService.saveItem(book);
return "redirect:/items";
}
정답은 준영속 상태이다.
준영속 상태 구별의 핵심은 식별자를 기준으로 영속상태가 되어서 DB에 저장된 적이 있는가이다.
준영속이라는 단어는 객체를 new 했거나, 안했거나를 기준으로 나누는 것은 아니다.
겉보기에넌 new Book() 으로 새 객체처럼 보이지만, ID 가 있는 객체이므로 DB 에 이미 저장된 적 있는 엔티티이다. 그러나 EntityManager 가 이를 관리하고 있지 않으므로 준영속 상태이다.
준영속 vs new 상태
구분 | new 상태 | 준영속 상태 |
ID 존재 | 없음 | 있음 |
영속성 컨텍스트 관리 여부 | ❌ | ❌ |
merge() 동작 | 새로운 row 생성 | 기존 row를 찾아 병합 |
변경 감지 가능 | ❌ | ❌ |
준영속 상태 동작 원리
new 상태인 객체와 준영속 상태의 객체는 merge() 라는 명령에서 동작하는 방식이 다르다.
new 상태인 객체는 merge() 를 호출할 때 완전히 새로운 엔티티를 만든다.
반면 준영속 상태의 엔티티는 DB 에서 기존 엔티티를 찾고 그 값을 준영속 상태의 객체로 변경한 후에 반환하다.
마치 준영속 상태의 객체가 영속상태가 되는 것과 같다.
변경 감지와 병합(merge)
병합은 모든 필드를 변경해버리고, 데이터가 없으면 null 로 업데이트 해버린다. (주의)
병합을 사용하면서 이 문제를 해결하려면, 변경 폼화면에서 모든 데이터를 항상 유지해야한다.
실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
따라서 엔티티를 변경할 때는 항상 변경감지를 사용해야한다.
- 컨트롤러에서 어설프게 엔티티를 생성하지 말자.
- 트랜잭션이 있는 서비스 계층에 식별자와 변경할 데이터를 전달하자.(파라미터 or dto)
- 트랜잭션이 있는 서비스 계층에서 영속 상태 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하자.
- 트랜잭션 커밋 시점에 변경감지가 실행된다.
@Transactional
public void updateItem(Long id, String name, int price) {
Book book = entityManager.find(Book.class, id); // 영속 상태
book.setName(name); // 변경 감지
book.setPrice(price);
}
The save method anti-pattern
@Transactional
public void saveAntiPattern(Long postId, String postTitle) {
⠀
Post post = postRepository.findById(postId).orElseThrow();
⠀
post.setTitle(postTitle);
⠀
postRepository.save(post);
}
jpa 에서는 엔티티 변경에 대해선 dirty checking 을 통해 update 하는 것이 권장되는 패턴이다.
이미 영속 상태에 있는 엔티티를 update 하려면 가장 효율적인 방식은 더킹 체킹이다.
영속 상태 엔티티는 필드값만 수성해도 자동으로 update 쿼리가 발생하니, 이 방식이 가장 효율적이다.
만약 save 를 호출한다면 pk 값이 이미 있기 때문에 내부적으로 merge 가 수행되고 그로 인해 불필요한
select 쿼리가 발생할 수 있으며 cascade 병합 오버헤드도 발생할 수 있다.
(https://vladmihalcea.com/best-spring-data-jparepository/)
마무리 요약
- new로 객체를 만들었더라도, 식별자(ID)가 존재하면 준영속 상태일 수 있다.
- 준영속 상태는 EntityManager가 관리하지 않는 과거 영속 객체
- 병합(merge)은 신중하게 사용할 것 → 모든 필드를 유지해야 함
- 실무에서는 변경 감지(dirty checking) 방식이 더 안전하고 권장된다.
'JPA' 카테고리의 다른 글
공통 인터페이스 기능 (1) | 2024.12.07 |
---|---|
양방향 연관관계와 연관관계 주인 (0) | 2024.12.05 |
[JPA] 영속성 컨텍스트 (0) | 2024.11.16 |