Spring Data JPA - 커스텀 레포지토리

Spring Data JPA

백기선님의 강의인 Spring Data JPA 강의를 듣고 공부한 내용을 정리한 글

커스텀 레포지토리

쿼리 메소드(쿼리 생성과 쿼리 찾아쓰기)로 해결이 되지 않는 경우 직접 코딩으로 구현이 가능하다.

  • 스프링 데이터 레포지토리 인터페이스에 기능 추가

  • 스프링 데이터 레포지토리 기본 기능 덮어쓰기 기능

  • 구현 방법

    • 커스텀 리포지토리 인터페이스 정의
    • 인터페이스 구현 클래스 만들기 (기본 접미어는 Impl)
    • 엔티티 레포지토리에 커스텀 레포지토리 인터페이스 추가

백문이 불여일타

@Entity
class Post (
    @Id @GeneratedValue
    val id:Long?,
    val title:String,
    @Lob
    val content:String,

    val created:LocalDate

){
    @OneToMany(mappedBy = "post",cascade = [CascadeType.ALL])
    val comments:MutableSet<Comment> = HashSet()

    fun addComment(comment:Comment){
        this.comments.add(comment)
        comment.post = this
    }



    override fun toString(): String {
        // toString이 찍을 때 comment도 가져옴. 이게 문제되는 이유는 하이버네이트가 쿼리로 comment를 가져옴 (쓰잘데기 없는 일을 함)
        return "Post(id=$id, title=$title content=$content created=$created)"
    }
}

Entity를 정의한다. Entity는 DB 테이블로 매핑될 모델 객체이다.

interface PostCustomRepository {

    fun findMyPosT(): List<Post>
}

평범한 인터페이스를 선언한다.

이름은 아무거나 해도 괜찮다.

그리고 방금 만든 커스텀 레포지토리를 구현한 객체를 생성한다. 이름은 접미어에 Impl을 붙인다.

@Repository
@Transactional
class PostCustomRepositoryImpl(
        val entityManager: EntityManager
) : PostCustomRepository {

    override fun findMyPosT(): List<Post> {
        println("custom findMyPost")
        return entityManager.createQuery("SELECT p FROM Post As p", Post::class.java).resultList
    }
}

아래와 같이 JpaRepository를 구현하는 PostRepository에 방금 만든 커스텀 레포지토리 인터페이스를 추가해준다.

interface PostRepository: JpaRepository<Post,Long>, PostCustomRepository {

}

준비는 끝났다.

테스트 코드를 작성해서 직접 테스트해보자.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class  PostRepositoryTest {

    @Autowired
    lateinit var postRepository:PostRepository

    @Test
    fun crud() {
        postRepository.findMyPost()
    }
}

잘 작동하고 우리가 찍어뒀던 prinltn이 표시될 것이다.

스프링 데이터 JPA의 기본 구현체 delete가 굉장히 느리다!?

	@Override
	@Transactional
	@SuppressWarnings("unchecked")
	public void delete(T entity) {

		Assert.notNull(entity, "Entity must not be null!");

		if (entityInformation.isNew(entity)) {
			return;
		}

		Class<?> type = ProxyUtils.getUserClass(entity);

		T existing = (T) em.find(type, entityInformation.getId(entity));

		// if the entity to be deleted doesn't exist, delete is a NOOP
		if (existing == null) {
			return;
		}

		em.remove(em.contains(entity) ? entity : em.merge(entity));
	}

여기서 주목할 부분은 가장 마지막 코드다.

EntityManager에 캐싱이 되어있는 경우가 아니라면 merge를 한다.

이 merge는 detached 상태(EntityManager의 관리를 벗어난 Entity 상태)의 객체를 다시 Persistance 상태로 바꿔주는 기능이다.

여기서 detached 상태의 객체는 과거에 Persistance 상태였기 때문에 (모종의 이유로 detached 상태가 되었다고 가정) DB 테이블에도 여전히 데이터가 남아있다.

이 때 detached 객체의 상태를 바꾸면서 DB 테이블과 객체간의 싱크를 맞출 때 사용하는 메소드가 바로 merge 메소드이다.

여기까지 작업한 후 삭제를 한다.

삭제를 한다고 해서 delete query가 바로 날아가는 것은 아니다.

이 때 객체의 상태를 remove로 바꾼다.

그럼 왜 바로 delete query를 안날리고 상태만 바꿀지 궁금할 수 있다.

그 이유는 가령 Post(게시글) 객체에 딸린 Comment 객체들 (cascade인 경우)을 다 같이 지워주어야 할 수 있다.

cascade인 경우를 삭제해주는 것도 EntityManager의 역할이기도 하다.

근데 만약에 삭제를 하려고 했는데 삭제할 필요가 없어졌다면 어떨까?

EntityManager의 관리를 받지 않고 delete query를 다이렉트로 날리면 다시 생성해줄건가? 그건 비효율적이다.

그래서 EntityManager를 사용하는 이유가 단순히 성능때문만은 아니다.

이러한 여러 상황에 대한 관리를 해주기 때문에 EntityManager가 필요한 것이고 그것을 보고 에이 JpaRepository delete 성능 개구림 ㅉㅉ라고 말하긴 어렵다는 것이다.

그래서 우리가 직접 기본 메소드를 재정의해보자

@Repository
@Transactional
class PostCustomRepositoryImpl(
        val entityManager: EntityManager
) : PostCustomRepository<Post> {

    override fun findMyPost(): List<Post> {
        println("custom findMyPost")
        return entityManager.createQuery("SELECT p FROM Post As p", Post::class.java).resultList
    }

    override fun delete(entity: Post) {
        println("custom delete")
        entityManager.remove(entity)

    }
}

기본 메소드를 재정의했다.

다시 테스트코드를 봐보자.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class  PostRepositoryTest {

    @Autowired
    lateinit var postRepository:PostRepository

    @Test
    fun crud() {
        val post = Post()
        post.title = "hibernate"
        postRepository.save(post)

        // insert, select 쿼리는 동작한다. 왜? 현재 트랜잭션에서 select로 가져오려면 insert된 데이터가 필요하니까.
        postRepository.findMyPost()

        // 여기서 delete Query를 별도로 날리지 않는 이유는 위 트랜잭션을 그냥 롤백하면 되니깐?
        // 하이버네이트의 경우 불필요한 쿼리는 아예 날리질 않는다.
        postRepository.delete(post)

        // flush를 하면 강제적으로 delete 쿼리를 날릴 수 있다.
        postRepository.flush()
    }
}

사실상 이번 강의에서 가장 중요한 부분이기도 한데, 왜 JPA가 쿼리를 날리고, 날리지 않는지를 이해해야한다.

이는 JPA Entity State에 대한 이해를 필요로 하며 하이버네이트의 정책도 이해해야한다.

Impl 말고 다른건 안되나?

가능하다.

@SpringBootApplication
@EnableJpaRepositories(repositoryImplementationPostfix = "Good", queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
class JpaApplication

fun main(args: Array<String>) {
    runApplication<JpaApplication>(*args)
}

repositoryImplementationPostfix의 값을 변경해주면 된다.

현재 EnableJpaRepositories 애노테이션에 정의된 repositoryImplementationPostfix의 디폴트 값은 Impl이다.

	/**
	 * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
	 * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
	 * for {@code PersonRepositoryImpl}.
	 *
	 * @return
	 */
	String repositoryImplementationPostfix() default "Impl";

Reference

인프런 백기선님의 스프링 Data JPA



© 2022. by minkuk

Powered by minkuk