JPA를 사용하다 보면 준영속 상태라는 개념을 접하게된다.
처음엔 어렵게 느껴지지만, 실무에서 엔티티를 수정할 때 꼭 이해하고 넘어가야 할 개념이다.
이 글을 읽고 나면 세 가지를 얻어갈 수 있다.
- new 로 만든 객체도 준영속일 수 있다는 것, 그리고 그 기준이 무엇인지 명확하게 이해하게 된다.
- 병합(merge) 이 왜 위험한지, 어떤 상황에서 예상치 못한 null 업데이트가 발생하는지 알게 된다.
- 실무에서 엔티티를 안전하게 수정하는 올바른 패턴이 무엇인지 코드로 확인할 수 있다.
JPA 의 엔티티 생명주기
JPA의 엔티티는 다음과 같은 생명주기를 가진다
- 비영속 (new): 아직 영속성 컨텍스트에 저장되지 않은 상태
- 영속: EntityManager를 통해 관리되고 있는 상태
- 준영속 (detached): 한 번 저장된 적이 있지만, 현재는 EntityManager가 관리하지 않는 상태
- 삭제: 삭제 명령이 수행된 상태
준영속 상태란?
준영속 상태는 이미 DB에 저장된 적 있는 객체인데, 현재는 영속성 컨텍스트에 의해 관리되지 않는 상태를 의미한다.
- 예) em.find() 로 조회 -> 영속 상태
- 트랜잭션 종료/ detach / clear -> 준영속 상태
이 상태에서는 다음 기능을 사용할 수 없다.
- 변경 감지(Dirty Checking) X
- 자동 UPDATE X
- 1차 캐시 관리 X
준영속 상태로 만드는 3가지 방법
em.detach(entity); // 특정 엔티티만 준영속
em.clear(); // 영속성 컨텍스트 전체 초기화
em.close(); // 영속성 컨텍스트 종료
그럼 이 객체도 준영속일까?
아래 코드를 보자
@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";
}
new Book()으로 생성했으니 비영속처럼 보이지만, 이 객체는 준영속 상태다.
준영속 상태 구별의 핵심은 new 키워드 여부가 아니라 식별자(ID) 의 존재 여부다.
ID가 있다는 것은 DB에 이미 해당 row 가 존재한다는 의미고, 그 객체를 EntityManager 가 현재 관리하고 있지 않다면 준영속이다.
준영속 vs new 상태
| 구분 | new 상태 | 준영속 상태 |
| ID 존재 | 없음 | 있음 |
| 영속성 컨텍스트 관리 여부 | X | X |
| merge() 동작 | 새로운 row 생성 | 기존 row를 찾아 병합 |
| 변경 감지 가능 | X | X |
두 상태 모두 영속성 컨텍스트가 관리하지 않는다는 점은 같지만, merge()를 호출했을 때 동작이 다르다.
new 상태의 객체를 완전히 새로운 row 를 만들고, 준영속 상태의 객체는 DB 에서 기존 엔티티를 찾아 값을 덮어쓴 뒤 영속 상태로 반환한다.
병합(merge) 의 위험성
merge 는 매우 편해 보이지만 실무에서 가장 위험한 메서드 중 하나다.
이유 : merge 는 모든 필드를 통째로 덮어쓴다.
병합은 모든 필드를 변경해버리고, 데이터가 없으면 null 로 업데이트 해버린다. (주의)
병합을 사용하면서 이 문제를 해결하려면, 변경 폼화면에서 모든 데이터를 항상 유지해야한다.
실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
예를 들어:
- 폼에서 name, price만 전달
- 나머지 필드는 null
merge 실행 시:
UPDATE book
SET name=?, price=?, author=NULL, stock=NULL ...
즉, 의도하지 않은 null업데이트가 발생할 수 있다.
변경 감지가 더 안전한 이유
JPA에서 엔티티 수정의 정석은 merge가 아니라 변경 감지다.
- 컨트롤러에서 어설프게 엔티티를 생성하지 말자.
- 트랜잭션이 있는 서비스 계층에 식별자와 변경할 데이터를 전달하자.(파라미터 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);
// commit 시점에 Dirty Checking → 자동 UPDATE
}
장점:
- 필요한 필드만 변경
- 불필요한 SELECT/merge 비용 없음
- null 덮어쓰기 위험 없음
- 성능 + 안정성 모두 우수
save() 메서드 안티패턴
@Transactional
public void saveAntiPattern(Long postId, String postTitle) {
Post post = postRepository.findById(postId).orElseThrow();
post.setTitle(postTitle);
postRepository.save(post); // 불필요
}
이미 영속 상태인데 save() 를 호출하면
- 내부적으로 merge 실행 가능
- 불필요한 SELECT 발생
- cascade 병합 오버헤드 증가
JPA에서는
영속 엔티티는 setter 만 변경하면 자동으로 업데이트 된다.
save() 호출은 불필요하다
(https://vladmihalcea.com/best-spring-data-jparepository/)
마무리
- new로 객체를 만들었더라도, 식별자(ID)가 존재하면 준영속 상태일 수 있다.
- 준영속 상태는 EntityManager가 관리하지 않는 과거 영속 객체
- 병합(merge)은 신중하게 사용할 것 → 모든 필드를 유지해야 함
- 실무에서는 변경 감지(dirty checking) 방식이 가장 안전하고 권장된다.
참고 자료
'JPA' 카테고리의 다른 글
| JPQL이 있는데 왜 Querydsl을 쓸까? (0) | 2026.02.25 |
|---|---|
| [JPA] 벌크 연산 - DB는 바뀌었는데 왜 값은 그대로일까? (0) | 2026.02.23 |
| 공통 인터페이스 기능 (2) | 2024.12.07 |
| 양방향 연관관계와 연관관계 주인 (2) | 2024.12.05 |
| [JPA] 영속성 컨텍스트 - 왜 JPA는 바로 DB 에 저장하지 않을까? (0) | 2024.11.16 |