티스토리 뷰

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 에서 EntityManager 클래스 다이어그램

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() 는 호출되지 않도록 하는 것이 좋다

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함