JPA save 메서드 삽질 기록

geon·2023년 8월 15일
0

우테캠 미션 수행 도중에 테스트에서 data JPA repository의 save 메서드를 남발하다가 에러를 엄청 많이 만났는데 오늘에 와서야 내부 동작을 생각해보게 되었다. 3가지 테스트를 실행해 보면서 save 메서드가 어떻게 동작하는지 알 수 있었다.

테스트에 사용한 엔티티 클래스

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

테스트 1

    @Test
    void idNull_generatedValue() {
        final Member member = new Member(null, "john");
        final Member savedMember = memberRepository.save(member);

        assertThat(member).isEqualTo(savedMember);
        assertThat(savedMember.getId()).isNotNull();
    }
Hibernate: insert into member (name,id) values (?,default)

먼저 data JPA repository의 save 메서드는 SimpleJpaRepositorysave 메서드를 사용한다. id가 null이면 em.persist를, id가 null이 아니면 em.merge를 호출한다.

  • SimpleJpaRepository 코드
	@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);
		}
	}

테스트 1의 경우는 id가 null이므로 em.persist를 호출하고, em.persist는 영속화한 엔티티를 그대로 반환하므로 membersavedMember가 같다. id는 리플렉션으로 끼워주는 게 아닐까 싶다.

테스트 2

    @Test
    void idNotNull_generatedValue() {
        final Member member = new Member(100L, "john");
        final Member savedMember = memberRepository.save(member);
        assertThat(member).isNotEqualTo(savedMember);
        assertThat(member.getId()).isEqualTo(100L);
        assertThat(savedMember.getId()).isNotEqualTo(100L);
    }
Hibernate: select m1_0.id,m1_0.name from member m1_0 where m1_0.id=?
Hibernate: insert into member (name,id) values (?,default)

테스트 2의 경우 em.merge가 호출된다. em.merge의 동작은 자세히 아는 건 아니지만, 대강 서술하면 다음과 같다.

1. id 값을 바탕으로 엔티티를 조회한다. (이때 `select` 쿼리 발생)
2-1. 해당 id를 가지는 엔티티가 존재하면 해당 엔티티에 값을 끼워 넣은 뒤 반환한다.
2-2. 해당 id를 가지는 엔티티가 존재하지 않으면 엔티티를 새로 생성해서 영속화한 뒤 반환한다.

테스트 2의 경우 100L을 id로 가지는 엔티티가 존재하지 않으므로 엔티티를 생성해서 반환한다. 따라서 savedMembermember는 다른 객체이다.
또한, 키를 자동 생성하므로 100L을 id로 쓰지 않는다. insert 쿼리에 id가 default로 들어간다.

테스트 3

    @Test
    void idNotNull_existingId_generatedValue() {
        final Member member1 = new Member(null, "john");
        final Member savedMember1 = memberRepository.save(member1);
        final Member member2 = new Member(member1.getId(), "nick");
        final Member savedMember2 = memberRepository.save(member2);

        assertThat(member1).isEqualTo(savedMember1);
        assertThat(member2).isNotEqualTo(savedMember2);
        assertThat(savedMember1).isEqualTo(savedMember2);
        assertThat(savedMember2.getName()).isEqualTo(("nick"));
        em.flush();
        em.clear();
    }
Hibernate: insert into member (name,id) values (?,default)
Hibernate: update member set name=? where id=?

테스트 3의 경우, member1의 경우는 em.persist가 호출된다. 따라서 member1savedMember1은 동일하다.
member2는 id가 null이 아니므로 em.merge가 호출된다. 해당 id를 가진 엔티티가 존재하므로 영속성 컨텍스트에서 해당 엔티티(savedMember1)을 가져와서 name 필드를 "nick"으로 변경한다. 따라서 member2savedMember2는 동일하지 않고, savedMember1savedMember2는 동일하다.
참고로, 영속성 컨텍스트에서 엔티티를 가져오기 때문에 select 쿼리가 발생하지 않는다.

번외 테스트

    @Test
    void idNotNull_generatedValueDisabled() {
        final Member member = new Member(100L, "john");
        final Member savedMember = memberRepository.save(member);
        em.flush();
        assertThat(member).isNotEqualTo(savedMember);
        assertThat(member.getId()).isEqualTo(100L);
        assertThat(savedMember.getId()).isEqualTo(100L);
    }
Hibernate: select m1_0.id,m1_0.name from member m1_0 where m1_0.id=?
Hibernate: insert into member (name,id) values (?,?)

GeneratedValue를 사용하지 않는 경우, 100L이 그대로 id로 들어간다. 이 경우에도 em.merge가 호출되기 때문에 매번 select 쿼리가 발생한다.

profile
뭐라도 적기

0개의 댓글