2 분 소요

동기

프로젝트를 하면서 다른 분이 작성한 코드를 본 적 있는데, DB에 저장돼있는 엔티티 즉, 애플리케이션으로 가져왔을 때 영속성 컨텍스트에 의해 관리되고 있는 엔티티를 수정한 후 업데이트해야 하는 케이스가 있었다. 이때 save() 또는 saveAll()을 호출했고 실제로 날아가는 쿼리가 insert 쿼리가 아닌 update 쿼리였고, 무조건 insert 쿼리로 날라갈거라는 편견이 있었어서 찾아보고 적어보게 됐다.

왜?

흔히 사용하는 JpaRepository를 상속하면 save()를 사용할 수 있게된다. 정의는 CrudRepository에서 하고 있다.

public interface CrudRepository<T, ID> extends Repository<T, ID> {
  <S extends T> S save(S entity);
  ...
}

구현부는 SimpleJpaRepository에 있다! isNew()로 새 엔티티인지 판별하고 맞으면 insert, 그렇지 않으면 update를 한다.
isNew()는 엔티티 상태에 따라서 새 엔티티 판별 기준이 달라지는데, 바로 아래에 작성한 isNew() 내용을 참고하자!

public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
  @Transactional
  @Override
  public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) { // 새 엔티티인가?
      em.persist(entity); // insert 쿼리 발생
      return entity;
    } else {
      return em.merge(entity); // update 쿼리 발생
    }
  }
  ...
}

isNew()

어떻게 새 엔티티인지 판별하는가는 case-by-case다! 아래 경우의 수를 참고하자.

  • 엔티티가 Persistable<ID>를 구현한 경우

Persistable<ID>를 구현한 엔티티라면 해당 엔티티 안에 구현한 isNew()를 기준을 따른다.
아래에서 나올 @Id, @Version 적용 여부는 기준으로 사용하지 않는다. (Persistable<ID>가 우선순위 더 높음)
아래 예시에서는 name 길이가 0이면 isNew()true임을 보장한다.

@Entity
public class Member implements Persistable<String> {

    @Id
    private String id;

    private String name;
    
    @Override
    public boolean isNew() {
        return name.length() == 0;
    }
}


  • @Version이 적용된 필드를 갖고있을 경우

해당 필드가 가진 값의 null 여부에따라 isNew()의 결과값이 달라진다.
null이면 true를, not null이면 false를 반환한다.
이때 @IdisNew()의 판단 기준으로 활용하지 않는다. (@Version이 우선순위 더 높음)
(참고) @Version은 낙관적 락(Optimistic Lock)에서 활용되기도 한다.

@Entity
public class Member {

  @Id
  private String id;

  @Version
  private Integer version;
}


  • @Id가 붙은 타입, 원시 타입(primitive type)이 아닌 경우

원시 타입이 아니라면 null을 허용하는 타입이므로, null 여부에 따라 isNew()의 결과값이 달라진다.
null이면 true를, not null이면 false를 반환한다.

@Entity
public class Member {

  @Id
  private long id;
}


  • @Id가 붙은 타입, 원시 타입(primitive type)인 경우

원시 타입이라면 null을 허용하지 않으므로, 값이 0이냐에 따라 isNew()의 결과값이 달라진다.
0이면 true를, 0이 아니면 false를 반환한다.

@Entity
public class Member {

  @Id
  private Long id;
}


결론

save()를 보고 항상 insert 쿼리를 날리겠지 했던게 꼭 그렇지만은 않다는걸 알 수 있었다!
코드를 보고 대충 기대하는 결과가 있을 수는 있지만, 내가 직접 확인해봤거나 확실한 레퍼런스를 체크해본게 아니라면 꼼꼼히 확인해볼 필요가 있겠다는 생각이 들었다.

Reference

카테고리:

업데이트:

댓글남기기