우테캠 미션 수행 도중에 테스트에서 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;
}
@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
메서드는 SimpleJpaRepository
의 save
메서드를 사용한다. 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
는 영속화한 엔티티를 그대로 반환하므로 member
와 savedMember
가 같다. id는 리플렉션으로 끼워주는 게 아닐까 싶다.
@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로 가지는 엔티티가 존재하지 않으므로 엔티티를 생성해서 반환한다. 따라서 savedMember
와 member
는 다른 객체이다.
또한, 키를 자동 생성하므로 100L을 id로 쓰지 않는다. insert
쿼리에 id가 default
로 들어간다.
@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
가 호출된다. 따라서 member1
과 savedMember1
은 동일하다.
member2
는 id가 null이 아니므로 em.merge
가 호출된다. 해당 id를 가진 엔티티가 존재하므로 영속성 컨텍스트에서 해당 엔티티(savedMember1
)을 가져와서 name
필드를 "nick"
으로 변경한다. 따라서 member2
와 savedMember2
는 동일하지 않고, savedMember1
과 savedMember2
는 동일하다.
참고로, 영속성 컨텍스트에서 엔티티를 가져오기 때문에 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
쿼리가 발생한다.