스프링/JPA

`SpringDataJPA` 기본 save 작동 과정 (merge와 persist를 중심으로 효율성 개선)

nomoreFt 2022. 2. 27. 19:02

기본적으로 SpringDataJPA의 기본 구현체인 save

    1. 새로운 엔티티면 저장(persist)
    1. 기존에 있으면 (db에서 한번이라도 퍼올렸던 경험) merge를 한다.

SimpleJpaRepository의 save 메서드 모습.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

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

        Assert.notNull(entity, "Entity must not be null.");

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

}

그럼 새로운 엔티티를 구분하는 방법은?

  • 식별자가 null (ex id = null)
  • 식별자가 자바 기본 타입 (long, int 등) 일 때, null이 안되므로 0인지 아닌지로 판단

    id값 널로 객체 생성해서 save할 때, 새로운 entity라 식별되는 사진
    @GenerateValue로 나중에 em.persist시에 id값이 등록된다.

    image

em.persist가 실행된 이후로 id값이 들어간 모습

image

문제, @GeneratedValue를 사용하지 않을 때, 식별자가 객체(String )일 때 , id에 특정 값을 설정할 때 merge가 발생하는 현상

why? 식별자가 객체(String)인데, null이 아니라 기존에 있는 값이라 생각. (DB에 있을거라고 가정)
step1. db에 있는지 select
step2. db에 없으면 insert

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
    @Id
    String id;
}
    @Test
    public void save(){
        Item item = new Item("A");
        itemRepository.save(item);
    }

select를 하기 때문에 굉장히 비효율적인 동작이 이루어진다.
또한 merge는 db에서 값을 모두 가져온 다음에, 비교해서 받은 Entity의 값으로 전체 교환을 하기 때문에
data update가 목적이라면 dirtyChecking을 사용하는게 좋다.

해결책 step1. 엔티티에 인터페이스 PersistableisNew 메서드 구현
해결책 step2. @CreatedDate 설정
@EntityListners(AuditingEntityListner.class) + @CreatedDate + Persistble 인터페이스의 isNew 구현한 모습

image

해결
새로 @OverrideisNew 메서드로 createdDate == null이라 새로운 객체라고 인식된 모습

image

결론

Entity 등록시 merge가 발동하면, select -> insert 과정으로 불필요한 select 문이 발생하므로 비효율적.
무조건 @GeneratedValue 설정을 하거나, 없다면 PersistableisNew 메서드 구현해서 SpringDataJPA 기본 save메서드가
merge를 피하게 만들자.