JPA

영속 상태의 오해

.도기 2025. 6. 5. 00:05

JPA를 사용하면서 영속 상태에 대해서 오해가 있었는데, 누가 보면 아주 귀여울 오해를 하고 있었다.

"이런 것도 몰랐냐?" 라고 생각 할 수 있지만, "이런 것도 몰랐구나~" 하는 너그러운 마음으로 봐줬으면 한다.

바보같은 오해

JPA에서 영속 상태는 엔티티의 식별자로 판단한다.

라고 생각했지만, 이것은 아주 바보같은 생각이었다.

 

JPA에서 영속 상태로 판단하는건 "영속화가 되었는가"이다. 영속화를 시키기 위해서는 식별자가 반드시 필요한 것이다.

Member m1 = new Member();      // 비영속
m1.setId(1L);                  // 여전히 비영속
em.persist(m1);               // 이제 영속 상태

엔티티에 식별자가 있어도, persist()를 호출 하지 않으면 영속상태가 되지 않는다. 즉, 영속성 컨텍스트에서 관리되지 않는다.

그럼 준영속 상태란 뭘까?

if (entity.id == null) {
    em.persist(entity); // 새 엔티티
} else {
    // DB에서 id로 조회
    // 조회된 영속 엔티티에 필드 복사
    // 복사된 영속 객체 리턴
}

준영속 상태는 한번, 영속성 컨텍스트에서 관리되었던 객체를 말한다.

즉, 한번 영속 상태였던 엔티티가, 더 이상 영속성 컨텍스트에서 관리되지 않는 상태를 말한다.

persist()를 통해 영속상태였던 객체가 detach(), clear(), close() 등을 통해서 준영속 상태가 된다.
이때 다시 영속상태로 되돌리려면 merge()를 사용하면 된다.

준영속 상태를 사용하는 경우

보통 수정 기능에서 준영속 상태를 사용한다.
JPA repository는 update() 메서드가 없다. 그래서 수정은 save() 또는 변경감지를 이용해야한다.

merge()를 이용한 save()

save() 메서드는 엔티티를 매개변수로 받는다. 보통 수정 요청은 dto로 받는다.

@Transactional
public void updateMember(MemberDto dto) {
    Member member = new Member(); //새로운 엔티티 생성
    member.setId(dto.getId());
    member.setName(dto.getName());

    memberRepository.save(member); // 내부적으로 merge
}

이런식으로 받을 것이다.
그리고 save를 해주게되면 JPA는 update 쿼리를 날린다. 

🤔 여기서 의문점

왜 새로운 객체를 만들어서 save()하는데 persist가 아니라 merge()로 동작하는거지?

영속성 컨텍스트에서 관리되지 않던 비영속 객체를 영속상태로 만들기 위해서는 persist를 사용해야한다.

위 코드에서 Member member = new Member(); 는 분명 새로운 객체를 만들어준다.

 

member.setId(dto.getId()); 그런데, 이 코드에서 member에  id를 주입한다.
그리고 save를 하게되면,

	@Override
	@Transactional
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);

		if (entityInformation.isNew(entity)) {
			entityManager.persist(entity);
			return entity;
		} else {
			return entityManager.merge(entity);
		}
	}

 

save의 내부 구조를 보면 
엔티티가 존재한다면 merge를 통해서 준영속 상태로 만들어준다.
isNew() 메서드는 쉽게말해서 id를 기반으로 엔티티를 검색한다.

그래서 준영속 상태의 객체는 다시 영속성 컨텍스트의 관리대상이 되기 때문에 변경감지 즉, 더티체킹이 일어나서 update 쿼리가 날라가는 것이다.

merge() 동작의 진실

merge()는 단순히 준영속 객체를 영속화 하는 것이 아니다.

 

정확히 말하자면

비영속 객체의 값을 복사하여 새로운 영속 객체를 생성하는 것이다.

즉, 원본 객체는 여전히 준영속 상태이다.

이로인해서 생길 수 있는 문제점에 대해서 알아보자.

1. 불필요한 필드까지 덮어쓸 수 있다.

수정 시, 일부 필드만 변경하고 싶은데, DTO가 다른 필드도 null로 전달하게 되면 기존의 데이터가 사라지고 null로 덮어씌워지게 된다.

2. 원본 엔티티가 아닌, 새 객체로 대체

merge()는 새 역속 객체를 반환한다. 때문에 원본 객체의 참조를 계속 쓸 경우 문제가 생길 수 있다.

merge()의 가장 큰 단점은 모든 데이터를 덮어쓰기 한다는 것이다. 때문에 merge()를 사용할 땐 "유지 해야하는 데이터가 있는지" 고민 후에 사용해야 한다.

수정은 변경감지를 이용하자

JPA는 더티체킹이라는 변경감지 기능을 제공한다. 이는 영속 상태의 엔티티를 수정하면 자동으로 변경이 감지되어서 db에 반영되는 기능이다.

 

merge()처럼 원본 객체의 복사본을 사용하지 않고, 원본 객체를 사용한다.

하지만 변경감지 기능도 트랜잭션 범위, 객체의 영속 상태에 따라서 동작방식이 달라질 수 있다. 이를 잘 인지하며 사용해야 한다.