[spring jpa] merge 와 변경감지
merge 와 변경감지
JPA 에서 DB 테이블을 수정하기 위해서 merge 와 변경감지 방법을 사용할 수 있습니다. 이 중 merge 는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 만들어 저장하는 방식이며, 변경감지는 영속 상태의 엔티티를 변경하는 방식입니다.
병합 : merge
병합은 준영속 상태의 엔티티를 다시 영속 상태로 변경하여 저장하게 합니다. 이 때 준영속 상태뿐만 아니라 비영속 상태의 엔티티도 저장할 수 있는데, 이때는 식별자가 없으므로 새로운 값으로 저장됩니다.
public class JpaMain {
static EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
public static void main(String[] args){
Member member = createMember("memberA", "회원1"); // 1
member.setUsername("회원명변경"); // 2
mergeMember(member); // 3
}
static Member createMember(String id, String username) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member();
member.setId(id);
member.setUsername(username);
em.persist(member);
tx.commit();
em.close();
return member;
}
static void mergeMember(Member member){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member mergeMember = em.merge(member);
tx.commit();
System.out.println("member = " + member.getUsername());
System.out.println("mergeMember = " + mergeMember.getUsername());
System.out.println("em.contains(member) = " + em.contains(member));
System.out.println("em.contains(mergeMember) = " + em.contains(mergeMember));
em.close();
}
}
createMember
를 통해member
가 테이블에 저장됩니다. 이후em.close()
로member
는 준영속 상태가 됩니다.member.setUsername("회원명변경");
를 통해 회원 이름을 변경했지만 수정 사항이 DB 에 반영되지 않습니다.mergeMember(member);
에서em.merge(member);
를 실행하면 엔티티의 식별자 값(id) 으로 1차 캐시를 조회하고, 엔티티가 없으면 DB 에서 엔티티를 조회해서 1차 캐시에 저장합니다. 그리고 조회한 영속 엔티티(mergeMember
) 에member
엔티티 값을 채워넣습니다.
병합이 간편한 것 같지만 병합을 사용하면 모든 속성이 변경됩니다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있습니다. 병합은 모든 필드를 교체하기 때문입니다. 이는 DTO 를 통해 원하는 값만 받아서 member 를 만든 후 병합을 하면, 기존 값이 없어지는 문제점이 발생합니다.
따라서 아래의 변경감지를 사용하는 게 좋습니다.
변경감지(dirty checking)
변경감지(dirty checking) 는 영속 상태의 엔티티의 값을 변경했을 때 변경한 값을 JPA 가 감지해서 자동으로 update 하는 방법입니다.
public class JpaMain {
static EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
public static void main(String[] args){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member();
member.setUsername("회원1");
em.persist(member); // 1
member.setUsername("회원1수정"); // 2
tx.commit();
em.close();
}
}
위 코드를 보면 1 에서 member
를 영속 상태로 만든 후 2 에서 member.setUsername("회원1수정");
로 이름을 변경합니다. 이후 별다른 저장 로직없이 tx.commit();
으로 커밋을 하면 member
의 이름은 “회원1수정” 으로 변경됩니다.
JPA 는 영속성 컨텍스트를 보관할 때, 최초 상태를 복사해서 저장해두는데, 이를 스냅샷이라고 합니다. 그리고 플러시 시점에서 스냅샷과 엔티티를 비교해서 변경된 지점을 찾습니다.
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저
flush()
가 호출됩니다. - 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾습니다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서
쓰기 지연 SQL 저장소
로 보냅니다. 쓰기 지연 저장소
의 SQL 을 DB 에 보냅니다.- DB 트랜잭션을 커밋합니다.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됩니다. 이렇게 생성된 UPDATE SQL 문의 형태는 아래와 같습니다.
UPDATE MEMBER
SET
NAME = ?,
AGE = ?,
GRADE =?,
...
WHERE
id =?
NAME 만 변경했으므로 UPDATE MEMBER SET NAME = ? WHERE id =?
으로 나갈 것 같지만 JPA 의 기본 전략은 모든 필드를 업데이트하는 것입니다. 이는 다음과 같은 장점이 있습니다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같기 때문에 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있습니다.
- DB 에 동일한 쿼리를 보내면 DB 는 이전에 한번 파싱된 쿼리를 재사용할 수 있습니다.
만약 필드가 많거나 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 UPDATE SQL 을 생성하는 전략을 선택하면 됩니다.
@Entity
@org.hibernate.annotations.DynamicUpdate
public class Member{...}
@org.hibernate.annotations.DynamicUpdate
은 하이버네이트 확장 기능으로, 수정된 데이터만 사용해서 동적으로 UPDATE SQL 을 생성합니다.
@DynamicInsert 도 있는데, 데이터를 저장할 때 데이터가 존재하는(null 이 아닌) 필드만으로 INSERT SQL 을 동적으로 생성하는 어노테이션입니다.
대략 컬럼이 30개 이상이 되면 기본 방법인 정적 수정 쿼리보다 @DynamicUpdate
를 사용한 동적 수정 쿼리가 빠릅니다. 물론 가장 정확한 방법은 직접 테스트해보는 것입니다. 하지만 한 테이블에 컬럼이 30개 이상 된다면 테이블 설계 상 책임이 적절히 분리되지 않았을 가능성이 높습니다.
merge 와 dirty checking
실무에서는 보통 업데이트 기능이 매우 제한적입니다. 그런데 병합은 모든 필드를 변경해버리고, 데이터 가 없으면 null 로 업데이트 해버립니다. 병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데 이터를 항상 유지해야 합니다. 실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭습니다.
따라서 엔티티를 변경할 때는 거의 모든 경우에 변경 감지를 사용하는 게 좋습니다.
merge 는 업데이트에서 사용한다기보다는, 엔티티가 영속 상태에서 벗어났을 때, 다시 영속상태로 복귀시키기 위해서 사용합니다.
댓글남기기