티스토리 뷰
spring data jpa 를 이용하면 repository 의 save() 메서드를 이용해서 엔티티를 저장한다. 이때 save() 메서드는 저장된 엔티티 객체를 반환하는데, 사실 파라미터로 전달된 엔티티 객체의 상태를 변경하고 그걸 그대로 반환하는거라 파라미터로 전달된 객체와 반환되는 객체가 같다.
@Test
void 동일한_객체를_반환한다() {
var person = new Person("lichking");
var savedPerson = personRepository.save(person);
assertThat(person).isEqualTo(savedPerson);
}
이런 이유로 따로 equals 를 오버라이딩하지 않은 상태에서 Person 객체를 비교하면 테스트가 통과한다. spring data jpa 는 결국 내부에서 jpa 의 EntityManager 를 이용하게 되는데 자동 구현해주는 save() 는 아래와 같이 동작한다.
em.persist(entity);
return entity;
EntityManager 의 저장하는 로직 자체가 void 메서드이기 때문에 그대로 반환하게 된다.
참고로 서로 다른 영속성 컨텍스트에서 로드된 엔티티를 대상으로 persist() 메서드를 호출하면 두번째 호출시에 예외가 발생한다.
@Test
void 동일한_객체를_반환한다() {
var person = new Person("lichking1");
var entityManager1 = entityManagerFactory.createEntityManager();
var tx = entityManager1.getTransaction();
tx.begin();
entityManager1.persist(person);
tx.commit();
var entityManager2 = entityManagerFactory.createEntityManager();
var tx2 = entityManager2.getTransaction();
tx2.begin();
person.rename("lichking2");
// 위와 동일하게 persist 를 호출하면 예외 발생!
entityManager2.persist(person);
tx2.commit();
}
이럴땐 persist() 가 아니라 merge() 를 호출해줘야 하는데 JPA 를 한 번 더 래핑하고 있는 spring data jpa 를 쓴다면 이 부분을 save() 메서드에 추상화해놨기 때문에 신경쓸 일이 없다.
// 위 코드에서 merge 로 변경하면 예외가 발생하지 않고 update 쿼리가 날아간다.
entityManager2.merge(person);
// spring 은 persist 와 merge 호출에 대한 분기문을 작성해놓아 사용자로 하여금 이 부분을 신경쓰지 않도록 하고 있다.
@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);
}
}
추상화된 부분으로 인해 spring data jpa 를 사용하는 입장에서는 persist() 와 merge() 를 구분하지 않고 그냥 사용할 수 있다. 다만 이때 주의할 점이 하나 있는데 persist() 를 호출할 때는 위에서 얘기한대로 파라미터와 반환 객체가 동일한데, merge() 를 호출할 때는 이 두 객체가 다르다. 때문에 호출 코드에서 save() 이후에 파라미터 객체를 핸들링하게 된다면 persist() 와 merge() 시에 동작이 달라지는 이슈가 발생할 수 있다.
사실 이는 EntityManager 의 메서드 시그니처만 봐도 어느정도 유추할 수 있다.
public void persist(Object entity);
public <T> T merge(T entity);
EntityManager 레벨에서 persist() 는 void 메서드이지만 merge() 는 T 를 반환한다. EntityManager 입장에서는 이 둘을 분명히 구분하게 하고 있지만 save() 로 추상화하는 과정에서 두 메서드의 동작을 통일해야했고, void 로 동작하는 persist() 를 호출함에도 굳이 return 구문을 넣은 것이다.
EntityManager 인터페이스는 계층에 따라 Hibernate 의 SessionImpl 이 구현하게 되는데(하이버네이트가 아닌 다른 구현체를 사용한다면 구현 클래스가 달라질 것이다.) 때문에 구현코드를 보고자한다면 SessionImpl 을 확인하면 된다.
Hibernate 의 내부 구현을 살펴보면 Merge 호출시 MergeEvent 라는 객체를 생성해서 처리하게 된다. 이 MergeEvent 객체를 살펴보자.
public class MergeEvent extends AbstractEvent {
private Object original;
private Serializable requestedId;
private String entityName;
private Object entity;
private Object result;
}
짧은 시간 내에 이해하기엔 방대한 코드가 들어있어 가볍게 필드만 살펴보면 처음에 MergeEvent 객체를 생성할때는 기존 entity 를 original 필드로 초기화하고, 이후 entity 필드를 초기화하고 새롭게 영속성 컨텍스트에 들어온 객체를 result 필드로 초기화한다. DETACHED 상태의 객체를 영속성 컨텍스트에 넣을 때 기존 객체를 넣는게 아니고 새롭게 만들어서 영속성 컨텍스트로 로드하고, 그 객체를 반환하기 때문에 결과적으로 파라미터로 들어간 객체와 반환된 객체가 달라지는 것이다. 이 로직은 DefaultMergeEventListener 에서 확인 할 수 있다.
달라진다고 표현하기는 하지만 기존 객체를 복사해서 새로운 객체를 만들기 때문에 두 객체의 레퍼런스가 달라진거 외에 변화를 느끼지 못할 수도 있다. 이 케이스가 문제를 일으킬 수 있는 케이스는 merge 과정에서 새로운 연관관계를 저장하는 경우인데, 이 때 파라미터로 들어간 DETACHED 객체에는 새로운 연관관계 객체의 id 가 채워지지 않는 반면 반환된 객체에는 id 가 채워져 있는걸 볼 수 있다. "분명히 연관관계 엔티티의 insert 쿼리까지 잘 날아갔는데 왜 id 가 null 이지" 라는 케이스가 생긴다면 merge() 가 호출된게 아닌가 의심해보는 것이 좋다.
# 정리
- spring data jpa 는 save() 호출시 분기에 따라 EntityManager 의 persist()/merge() 호출을 결정한다
- persist() 가 실행된 경우엔 파라미터와 반환 객체가 동일하다
- merge() 가 실행된 경우엔 파라미터와 반환 객체가 다르다
- 그러니 save() 를 호출하는 경우엔 persist()/merge() 구분없이 반환객체를 이용하는 것이 좋다
- 더 좋은건 가급적 merge() 는 호출되지 않도록 하는 것이 좋다
'Java > jpa' 카테고리의 다른 글
spring boot 2.x hibernate 5.x 에서 매핑키만 있고 데이터는 없는 경우 전체 엔티티 조회 실패 (0) | 2024.04.30 |
---|---|
@DynamicUpdate 는 언제 써야할까 (0) | 2023.01.10 |
non-primary key 를 이용해 연관관계 설정시 Serializable 인터페이스 구현 (1) | 2021.06.16 |
JPA 양방향 연관관계 매핑 (0) | 2020.04.25 |
JPA 조회조건 객체 매핑 (0) | 2017.12.15 |
- Total
- Today
- Yesterday
- 정규표현식
- spring cloud
- TEST
- generics
- mariadb
- Design Pattern
- Spring
- frontend개발환경
- EffectiveJava
- frontcode
- java8
- toby
- java
- go-core
- Kotlin
- Git
- db
- backend개발환경
- servlet
- clean code
- JPA
- http
- MySQL
- OOP
- JavaScript Core
- DesignPattern
- Jackson
- programming
- code
- javascript
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |