@Test
@DisplayName("회원 등록 테스트")
void insertMemberTest() {
// given
Member member = Member.builder().id("id1").username("name1").age(20).build();
// when
Member saveMember = memberRepository.save(member);
// then
assertEquals(saveMember.getUsername(), "초원");
}
쿼리 로그를 찾아보니 insert 쿼리 전에 select 쿼리가 실행되고 있었다.
Hibernate:
select
member0_.id as id1_0_0_,
member0_.age as age2_0_0_,
member0_.name as name3_0_0_
from
member member0_
where
member0_.id=?
Hibernate:
insert
into
member
(age, name, id)
values
(?, ?, ?)
나는 insert 를 하기 전에 중복되는 값이 있는지 확인하고 넣을 것이기 때문에 이 select 문이 필요없다. 그래서 이 insert 문이 왜 발생하는 것이고, 어떻게 하면 생략하도록 할 수 있는지 알아보기로 했다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager em;
// ...
@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);
}
}
}
문제의 원인은 JpaRepository의 구현체인 SimpleJpaRepository 클래스에서 save() 메서드의 동작 방식에서 확인할 수 있다.
entityInformation.isNew()를 호출하여 전달 받은 객체가 new 상태라면 persist() (신규 저장) 하고, 새로운 객체가 아니면 merge() (병합) 한다.

이 isNew() 메서드가 호출되면 위의 JpaMetamodelEntityInformation 과 AbstractEntityInformation 클래스의 isNew() 메서드가 호출된다.
public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {
private final Optional<SingularAttribute<? super T, ?>> versionAttribute;
public boolean isNew(T entity) {
if (!this.versionAttribute.isEmpty() &&
!(Boolean) this.versionAttribute.map(Attribute::getJavaType)
.map(Class::isPrimitive)
.orElse(false)) {
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return (Boolean)this.versionAttribute.map((it) -> {
return wrapper.getPropertyValue(it.getName()) == null;
}).orElse(true);
} else {
return super.isNew(entity);
}
}
}
먼저 JpaMetamodelEntityInformation의 isNew() 메서드의 동작 과정을 살펴보자.
Entity 에 @Version 어노테이션을 사용하고 있는 필드가 있는지 확인
관련 필드가 없으면 versionAttribute.isEmtpy() 가 true 를 리턴하고 super.isNew(entity) 가 호출된다.
관련 필드가 있는 경우에도 primitiveType 이라면 super.isNew(entity) 가 호출된다.
2, 3 의 경우가 아니면 @Version 어노테이션을 사용하는 필드가 있고, primitive type 이 아닌 경우이다. 이 경우에는 해당 필드 값의 null 여부를 리턴한다.
public abstract class AbstractEntityInformation<T, ID> implements EntityInformation<T, ID> {
// ...
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
}
다음으로 super.isNew() 에 해당하는 AbstractEntityInformation 의 isNew() 메서드를 보자.
먼저 @Id 어노테이션이 달려 있는 필드를 확인한다.
만약 이 필드가 primitive type 이 아니라면 null 여부를 리턴한다. (나의 경우에는 id 값이 String 타입이여서 이곳에서 false 가 리턴되면서 새로운 엔티티가 아닌 것으로 인식되었다)
만약 primitive type 중 Number 의 하위 타입이면 0인지 확인한다.
@Version 필드가 없고 @Id 필드에 값이 있는 상태에서 저장할 경우 (Number 타입이면 0이 아닐 경우) isNew() 의 반환 값이 false 를 반환한다.
그래서 isNew() 가 false 를 반환할 경우 새로운 엔티티가 아닌 것으로 인식되어 save() 메서드가 em.merge() 를 호출한다.
merge() 메서드는 Detached 상태의 엔티티를 Managed 상태로 만들기 위해 식별자 값으로 조회 (select 문 발생) 한 뒤, 조회된 객체가 있으면 Update 하고, 없으면 새로 Insert 를 하여 병합한다.
Entity 클래스에서 Persistable 인터페이스를 구현하면 된다.
package org.springframework.data.domain;
public interface Persistable<ID> {
@Nullable
ID getId();
@Override
boolean isNew();
}
@Entity
public class User implements Persistable<String> {
@Id
private String id;
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return true; //하지만 이렇게 하면 update가
//필요한 경우에도 insert 문이 나간다.
}
}
여기서 isNew() 를 무조건 true 를 리턴하게 되면 select 하지 않고 insert 하긴 하지, update 을 할 때에도 insert 를 수행하게 된다.
그러면 어떻게 해야될까?
@Entity
public class User implements Persistable<String> {
@Id
private String id;
/* save 시 Select 날리는 것을 방지 */
@Transient
private boolean isNew = true;
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
}
isNew 가 true 가 되는 기준을 id 존재 여부가 아닌
@PostLoad)@PrePersist)으로 변경해준다.
@Transient어노테이션을 붙여 해당 데이터를 테이블의 컬럼과 매핑 시키지 않는다

이렇게 하면 전에 처럼 JpaMetamodelEntityInformation 의 isNew() 메서드가 아닌 JpaPersistableEntityInformation의 isNew() 가 호출된다.
public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID> extends JpaMetamodelEntityInformation<T, ID> {
public boolean isNew(T entity) {
return entity.isNew();
}
}
그리고 이 메서드에서는 앞서 우리가 구현했던 isNew() 메서드가 호출된다.
https://gmoon92.github.io/jpa/2019/09/29/what-is-the-transient-annotation-used-for-in-jpa.html
https://ttl-blog.tistory.com/191
https://programmer-chocho.tistory.com/80