본문 바로가기
Java & Kotlin/Spring

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

by devson 2023. 1. 9.

통상적으로 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를 쓰되,

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

 
 

댓글