Spring Data JPA - save()

Spring Data JPA

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

Save 메소드에 대한 심도 깊은 고찰

JpaRepository의 save()는 단순히 새 엔터티를 추가하는 메소드가 아니다.

save()는 업데이트를 위한 용도로도 사용될 수 있다.

Transient 상태의 객체라면 EntityManager.persist() Detached 상태의 객체라면 EntityManager.merge()

그러면 어떻게 Transient인지 Detached인지를 판단할 수 있을까?

먼저 Entity의 @Id 프로퍼티를 찾고 해당 프로퍼티가 null이면 Transient 상태로 판단하고 null이 아니면 Detached로 판단한다.

Entity 생명주기를 까먹었기 떄문에 다시 리마인드할 겸 정리해둔다

  • Transient : 새롭게 만들어진 객체. 하이버네이트나 DB 둘다 얘의 정보를 모름.

  • Persistent : 영속성이라고도 불리우는 상태. Persistent 컨텍스트가 관리를 하는 상태이다. 상태를 관리하거나 상태라면 필요한 경우 DB에 싱크를 한다.

  • Detached : 한 번이라도 DB에 Persistent가 되었던 객체이다. 즉 이 객체의 id가 테이블에 매핑이 되는 경우.

Detached.merge()의 오묘함

여기서 한 가지 알아둬야할 부분은 JPA에서 Detached상태는 merge()를 할 때 반드시 update query만 날리는 것은 아니다.

가령 해당 Entity가 DB 테이블에 존재하지 않는 경우라면 insert query를 날리기도 한다.

그러면 여기서 의문이 생길 수 있다.

Persistent를 왜 써야하지? 그냥 merge로 다 하면 되는 거 아닌가?

merge와 Persistent의 차이를 이해하기 위해서는 아래의 코드를 보아야한다.

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

    @Autowired
    lateinit var postRepository:PostRepository

    @PersistenceContext
    lateinit var entityManager: EntityManager

    @Test
    fun entityLifeCycleTest() {
        val post = Post()
        post.title = "JPA"
        val savedPost = postRepository.save(post) // persist

        assertThat(entityManager.contains(post)).isTrue()
        assertThat(entityManager.contains(savedPost)).isTrue()
        assertThat(savedPost == post)

        val postUpdate = Post()
        postUpdate.id = post.id
        postUpdate.title = "Hibernate"
        val updatedPost = postRepository.save(postUpdate) // merge

        assertThat(entityManager.contains(updatedPost)).isTrue()
        assertThat(entityManager.contains(postUpdate)).isFalse()
        assertThat(postUpdate != updatedPost)

    }
    ...
}

id가 없는 post 객체의 경우 Transient 상태에서 save가 될 경우 Persistent 상태로 변경된다.

이 때 save 메소드의 매개변수인 post 객체는 EntityManager에 캐싱된다.

이 때 save 메소드의 반환값인 savedPost 객체와 post 객체는 서로 동일한데, 그 이유는 save 메소드의 반환 값이 post 객체를 그대로 반환하기 때문이다.

그래서 savedPost와 post 모두 EntityManager가 관리하고 있으며 두 객체의 주소를 비교해보면 둘은 사실 같은 주소, 즉 같은 인스턴스임을 알 수 있다.

스크린샷 2020-07-05 오후 6 40 52

실제로 두 인스턴스의 hash값을 찍어보면 같다.

자 그러면 아래의 save는 왜 merge일까?

그건 당연히 postUpdate가 id값을 갖고 있기 때문이다.

앞서 이야기 했듯이, JPA는 Entity가 DB 테이블에 해당 도메인의 Id 값이 존재하지 않는 경우 Transient 상태로 판단한다.

그러나 밑의 postUpdate의 경우 post의 id를 갖고 있기 때문에 이 save 메소드는 EntityManager가 merge()를 호출하게 되는 것이다.

가급적이면 반환된 인스턴스를 사용해야하는 이유

여기서 백기선님의 조언이 나오는데 가급적이면 파라미터로 던진 인스턴스 대신 save 메소드 반환 인스턴스를 사용할 것을 권장하고 있다.

왜 그럴까?

여기에는 소름돋는 사실이 하나 있다.

스크린샷 2020-07-05 오후 7 10 55

스크린샷 2020-07-05 오후 7 10 59

당연하다고 생각할 수도 있지만 postUpdate는 EntityManager가 관리하고 있는 대상이 아니기 때문에 이 인스턴스의 값을 바꿔봐야 쿼리에 영향을 미치지 않는다.

스크린샷 2020-07-05 오후 7 11 59 스크린샷 2020-07-05 오후 7 12 18

그러나 save()의 반환 값인 updatedPost의 값을 바꾸게 될 경우 객체의 상태가 반영된다.

EntityManager가 이 객체를 관리하고 있기 때문에 가급적이면 반환값의 객체를 사용하는 것이 옳다.

그래야 만에 하나 발생하는 에러를 미연에 방지할 수 있게 된다.

이것은 일종의 좋은 코딩 습관이라고도 할 수 있겠다.

Reference

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



© 2022. by minkuk

Powered by minkuk