JpaRepository의 save(S entity) 동작 원리

Spring Data JPA에서 새로운 Entity인지 판단하는 방법
SpringJPA
avatar
2025.04.02
·
6 min read

개요

public Member saveMember(){
    // Member 객체 생성
    Member member = createMember();
    
    // 저장
    return memberRepository.save(member);
}

위와 같이 save메소드를 호출 및 리턴할 때 생성된 객체와 동일한 객체가 리턴되는지, 아니면 새로운 객체를 만들어서 리턴시키는지 궁금해졌다. 궁금증 해결을 위해 해당 메소드의 동작 원리를 파악해보자!

 

구현체 분석

package org.springframework.data.repository;

/**
 * Interface for generic CRUD operations on a repository for a specific type.
 */
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	/**
	 * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
	 * entity instance completely.
	 *
	 * @param entity must not be {@literal null}.
	 * @return the saved entity; will never be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
	 * @throws OptimisticLockingFailureException when the entity uses optimistic locking and has a version attribute with
	 *           a different value from that found in the persistence store. Also thrown if the entity is assumed to be
	 *           present but does not exist in the database.
	 */
	<S extends T> S save(S entity);
    
}

save() 메서드는 CrudRepository에 선언되어 있다. 구현이 되어 있는 상태가 아니기 때문에 어떻게 동작하는지 파악이 어렵다.

 

package org.springframework.data.jpa.repository.support;
    
/**
 * Default implementation of the {org.springframework.data.repository.CrudRepository} interface. 
 * This will offer you a more sophisticated interface than the plain {EntityManager}.
 */
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

	@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);
		}
	}
    
}

CrudRepository의 구현체인 SimpleJpaRepository로 이동해서 확인해보자. 

entity.isNew 이면 entityManager.persist 를 호출하고, 파라미터로 전달한 엔티티 객체를 그대로 반환하는 것을 알 수 있다.  entity.isNew가 아니면 entityManager.merge 를 호출하고, 파라미터로 전달한 엔티티 객체가 아닌 다른 객체를 리턴하는 것을 알 수 있다. 

merge의 경우, save 메서드를 통해 리턴된 엔티티만 영속성 컨텍스트가 관리한다. merge는 준영속 상태의 엔티티를 DB에서 조회하고,  DB에서 조회된 엔티티를 영속성 컨텍스트에 넣고 해당 엔티티를 반환한다. 따라서 반환된 엔티티는 영속성 컨텍스트가 관리하지만, 기존 인자로 넘겨준 엔티티는 여전히 준영속 상태이기 때문에 이를 주의해야 한다!

isNew는 왠지 새로운 엔티티인지 구별하는 메소드 인 것 같은데 어떻게 새로운 엔티티 인지 구별할 수 있을까?

 

package org.springframework.data.repository.core.support;

/**
 * Base class for implementations of {EntityInformation}. Considers an entity to be new whenever
 * {getId(Object)} returns {null} or the identifier is a {Java primitive} and
 * {getId(Object)} returns zero.
 */
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));
	}

}

식별자가 Primitive 타입이 아닌 참조 타입(Long, Integer, ...) 일 때는 id가 null인지 아닌지로 판별한다. 만약 id == null 이라면 새로운 엔티티라고 판단하고 true를 반환한다.

식별자가 기본 타입(long, int, ...)일 경우에는 id가 0인지 아닌지로 판별한다. 만약 id == 0L 이라면 새로운 엔티티라고 판단하고 true를 반환한다.

그렇다면 만약 id를 @GenerateValue가 아닌, 직접 할당하는 경우에는 어떻게 할까? 이런 경우에는 merge가 호출되는데, merge는 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지를 하기 때문에 이는 아주 비효율적이다. 따라서 Persistable 를 사용하여 새로운 엔티티를 확인를 로직을 짜는게 더 효율적이다. @CreatedDate를 같이 사용하여 @CreatedDate에 값이 없으면 새로운 엔티티로 판단하는 로직을 작성하는 방법이 있다. 

// Persistable 인터페이스
package org.springframework.data.domain;

public interface Persistable<ID> {
    ID getId();
    boolean isNew();
}
// Persistable 구현
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
    @Id
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;

    public Item(String id) {
            this.id = id;
    }

    @Override
    public String getId() {
            return id; 
    }

    @Override
    public boolean isNew() {
            return createdDate == null;
    }
}

 

참고

인프런 김영한 강사님 - "실전! 스프링 데이터 JPA"







- 컬렉션 아티클