JPA

[JPA] 준영속 상태란? 실무에서 왜 중요할까?

경딩 2025. 5. 8. 23:36

JPA를 사용하다 보면 준영속 상태라는 개념을 접하게된다.

처음엔 어렵게 느껴지지만, 실무에서 엔티티를 수정할 때 꼭 이해하고 넘어가야 할 개념이다.

 

이 글을 읽고 나면 세 가지를 얻어갈 수 있다.

  • new 로 만든 객체도 준영속일 수 있다는 것, 그리고 그 기준이 무엇인지 명확하게 이해하게 된다.
  • 병합(merge) 이 왜 위험한지, 어떤 상황에서 예상치 못한 null 업데이트가 발생하는지 알게 된다.
  • 실무에서 엔티티를 안전하게 수정하는 올바른 패턴이 무엇인지 코드로 확인할 수 있다.

JPA 의 엔티티 생명주기

JPA의 엔티티는 다음과 같은 생명주기를 가진다

  1. 비영속 (new): 아직 영속성 컨텍스트에 저장되지 않은 상태
  2. 영속: EntityManager를 통해 관리되고 있는 상태
  3. 준영속 (detached): 한 번 저장된 적이 있지만, 현재는 EntityManager가 관리하지 않는 상태
  4. 삭제: 삭제 명령이 수행된 상태

준영속 상태란?

준영속 상태는 이미 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) 방식이 가장 안전하고 권장된다.
 

 

 

 

참고 자료