Java & Kotlin/Spring

[Spring Data JPA] entity update 후 JpaRepository.save 호출에 관하여

devson 2023. 1. 9. 13:30

통상적으로 Spring Data Jpa를 사용하면 Hibernate를 사용하게 되고, 그로인해 dirty checking도 사용하게 된다.

그렇기 때문에 아래와 같이 코드에 따로 update를 명시하지 않아도, entity의 변경을 감지하여 update 문이 실행된다.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UpdatePostService {
    private final PostRepository postRepository;

    public UpdatePostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public void update(Long postId, String title, String content) {
        var post = this.postRepository.findById(postId).orElseThrow();
        post.update(title, content);
    }
}

interface PostRepository extends JpaRepository<Post, Long> {
}

 

하지만 위 코드의 문제는 Spring Data JPA 의존적인 코드라는 점이다.

만약에 위 PostRepository의 구현체를 Spring Data JDBC와 같은 다른 ORM framework나 MyBatis와 같은 SQL mapping framework와 같이 dirty checking을 지원하지 않는 구현체로 갈아끼운다면 위 코드에서 update 문이 실행될 수 있을까?

분명 그렇지 않을 것이다. 이는 OCP를 명백하게 위반하는 부분이다.

 

그래서 이러한 Spring Data JPA 의존적인 코드를 지양하고자 아래와 같이 entity update 후 직접적으로 repository의 save 메서드를 호출하는 방법이 있을 것이다.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UpdatePostService {
    private final PostRepository postRepository;

    public UpdatePostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public void update(Long postId, String title, String content) {
        var post = this.postRepository.findById(postId).orElseThrow();
        post.update(title, content);
        this.postRepository.save(post); // <-- explicit save
    }
}

interface PostRepository extends JpaRepository<Post, Long> {
}

이렇게하면 repository를 다른 구현체로 갈아끼워도 호환성에 있어서 문제가 없을 것이다.


까지는 여러 사람들이 생각을 해봤겠지만,

실제로 Spring Data JPA를 사용하면서 위와 같이 save를 명시적으로 호출하면 내부적으로 뭔가가 더 일어나게 된다.

 

JpaRepository의 구현체인 SimpleJpaRepositorysave 메서드를 살펴보면 아래와 같이 entity 상태에 따라 분기를 타게되는데,

현재 살펴보는 예제의 entity는 이미 DB로부터 조회된 entity이기 때문에 em.merge가 실행될 것이다.

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

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

	// ...
}

 

merge가 실행되고 나서는 내부적으로 MergeEvent가 생성되고 MergeEventListener가 이 이벤트를 처리하게된다.

package org.hibernate.event.internal;

public class DefaultMergeEventListener
		extends AbstractSaveEventListener<MergeContext>
		implements MergeEventListener {

	// ...

	public void onMerge(MergeEvent event, MergeContext copiedAlready) throws HibernateException {
		// 여러 과정을 거쳐 아래 entityIsPersistent 메서드 호출
	}
    
	protected void entityIsPersistent(MergeEvent event, MergeContext copyCache) {
		LOG.trace( "Ignoring persistent instance" );

		//TODO: check that entry.getIdentifier().equals(requestedId)

		final Object entity = event.getEntity();
		final EventSource source = event.getSession();
		final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );

		copyCache.put( entity, entity, true );  //before cascade!

		cascadeOnMerge( source, persister, entity, copyCache );
		copyValues( persister, entity, entity, source, copyCache );

		event.setResult( entity );
	}
    
	// ...
}

이는 불필요한 오버헤드를 일으키게 되며 오히려 save를 직접 호출하지 않는 것이 더 좋을 수 있다.

 

 

그럼 이를 어떻게 처리하면 좋을까?

Hypersistence Optimizer의 개발자 Vlad Mihalcea의 블로그 글에서는 이를 save method anti-pattern이라고 하며

JPA에는 save가 없기 때문에 이를 사용하지 말고, hypersistence-utils-hibernateHibernateRepositoryJpaRepository와 같이 사용하라고 얘기한다.

 

import io.hypersistence.utils.spring.repository.HibernateRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class UpdatePostService {
    private final PostRepository postRepository;

    public UpdatePostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public void update(Long postId, String title, String content) {
        var post = this.postRepository.findById(postId).orElseThrow();
        post.update(title, content);
        this.postRepository.update(post); // HibernateRepository.update
    }
}

@Repository
interface PostRepository extends HibernateRepository<Post>, JpaRepository<Post, Long> {
}

(예제 코드는 여기를 참고)

 
 

 

이렇게 함으로써 JPA에 존재하지 않는 save를 사용하는 것을 피하게 하고, 성능에 있어서 장점을 제공한다고 한다.

 


앞서 소개한 방법은 이런 방식도 있구나해서 정리를 한 것이지만, 너무 프레임워크 관련 지식이 필요 이상으로 많아지는 것 같다.

요청 처리에 있어서 너무 많은 리소스를 잡아먹지 않는 이상 그대로 JpaRepository.save를 쓰되,

트래픽이 어마무시하게 많아지거나 이로인한 문제가 발견되었을 때 갈아끼워도 되지 않을까 싶다. 🤔